Part of #839 — [Audit][Round 2] A++ umbrella.
Phase: P1 · Finding: R2-3 · Predecessors: #783 (closed), #784 (closed)
Problem
Round 1 closed the worst RHI native-handle leaks (#783 added the compute facet, #784 eliminated most of INativeHandlesContext). Three residual leaks survive that prevent true LSP — any non-Vulkan backend would still have to fake Vulkan's memory layout.
Evidence (all verified at HEAD)
Leak 1 — lpv_backend.zig:13 downcasts *anyopaque → *VulkanContext
pub fn fromVulkanRHI(rhi: rhi_pkg.RHI) LPVBackend {
return .{ .vk_ctx = @ptrFromInt(rhi.nativeHandles().getBackendContext()) };
}
A high-level lighting module directly names VulkanContext (a backend-internal type) and downcasts the abstract context pointer. Any non-Vulkan backend would have to masquerade as VulkanContext.
Leak 2 — ComputeBuffer / ComputePipeline carry raw Vulkan-shaped u64 fields
modules/engine-rhi/src/rhi.zig:666-678:
pub const ComputeBuffer = extern struct {
buffer: u64, // obviously VkBuffer
memory: u64, // obviously VkDeviceMemory
size: u64,
...
};
pub const ComputePipeline = extern struct {
pipeline: u64, // VkPipeline
layout: u64, // VkPipelineLayout
descriptor_pool: u64, // VkDescriptorPool
descriptor_set_layout: u64, // VkDescriptorSetLayout
descriptor_sets: [MAX_FRAMES_IN_FLIGHT]u64,
...
};
Then rhi_vulkan.zig:1007-1201 immediately casts them back: c.vkCmdBindPipeline(cmd, ..., @ptrFromInt(pipeline)), c.vkCmdBindDescriptorSets(cmd, ..., @ptrFromInt(pipeline_layout), ...), etc.
Leak 3 — imgui_backend.zig:31-45 consumes 6 native handles in a high-level UI module
const native = rhi.nativeHandles();
const init_info = c.ImGui_ImplVulkan_InitInfo{
.instance = @ptrFromInt(native.getInstance()),
.physical_device = @ptrFromInt(native.getPhysicalDevice()),
.device = @ptrFromInt(native.getDevice()),
.queue = @ptrFromInt(native.getQueue()),
.descriptor_pool = @ptrFromInt(native.getDescriptorPool()),
.render_pass = @ptrFromInt(native.getUiRenderPass()),
...
};
engine-ui (high level) directly constructs 6 distinct Vulkan handle types from INativeHandlesContext. ImGui's Vulkan backend inherently needs these — but the consumption should live behind an IImGuiBackend shim in engine-graphics, not in engine-ui.
Bonus leak — RhiError.VulkanError names a specific backend in the abstract error set
modules/engine-rhi/src/rhi_types.zig:7: VulkanError sits beside abstract variants (OutOfMemory, GpuLost, SurfaceLost). Callers can't pattern-match meaningfully when the abstract type names a concrete backend.
Fix
Decision required first (call it out in the PR description):
Either (a) commit to Vulkan-only-forever and stop pretending the abstraction is portable, OR (b) close the leaks. Both are valid. The current state — Vulkan-only-forever plus a fake abstraction — is the worst of both.
Assuming (b):
-
Promote LPV behind the RHI. Add ILightingContext (or extend IEffectsContext) with drawLPVGrid(params), uploadLPVData(data). Rewrite lpv_backend.zig against the abstract facet. Delete the @ptrFromInt(rhi.nativeHandles().getBackendContext()) downcast.
-
Make ComputeBuffer / ComputePipeline opaque. Change public fields to handle: u32 (matching BufferHandle/TextureHandle/ShaderHandle discipline). Move the VkBuffer/VkDeviceMemory/VkPipeline/etc. fields into a backend-internal struct (vulkan/compute_resources.zig). Update rhi_vulkan.zig:1007-1201 to look them up via compute_resources.get(handle) instead of @ptrFromInt(pipeline).
-
Move ImGui Vulkan consumption into engine-graphics. Create modules/engine-graphics/src/vulkan/imgui_vulkan_bridge.zig that consumes INativeHandlesContext and constructs ImGui_ImplVulkan_InitInfo. engine-ui/imgui_backend.zig calls rhi.initImGuiBackend(impl_data) (a new IImGuiContext method). Delete the 6 native-handle reads from engine-ui.
-
Rename RhiError.VulkanError → RhiError.BackendError (or remove and replace with backend-agnostic DeviceError). Update the ~10 callers in engine-graphics.
-
(Optional, related) Migrate the anyerror! paths at rhi.zig:369, 596, 615, 689, 1096, 1109, 1147, 1171 to typed RhiError! so callers can pattern-match.
Verification
nix develop --command zig build test (includes shader validation)
- Required: headless screenshot baseline at low/medium/high presets before and after — LPV lighting, ImGui rendering, and compute meshing must all be visually identical
nix develop --command zig build test -- --test-filter "RHI" (will exercise R2-4 if landed first)
grep -rE "@ptrFromInt\(.*nativeHandles|@ptrCast.*VulkanContext" modules/ should return zero matches outside modules/engine-graphics/src/vulkan/
Constraints
- Behavior-preserving end-to-end. The Vulkan path must stay visually and functionally identical.
- One PR per leak (4 PRs total) — each independently revertable.
- Order recommendation: land R2-4 (RHI contract tests) first so this refactor is guarded.
- Conventional commits:
refactor(engine-rhi): make ComputePipeline opaque, refactor(engine-graphics): move ImGui Vulkan bridge, etc.
- Keep worker-thread RHI isolation intact.
Notes
Tracking: #839
Part of #839 — [Audit][Round 2] A++ umbrella.
Phase: P1 · Finding: R2-3 · Predecessors: #783 (closed), #784 (closed)
Problem
Round 1 closed the worst RHI native-handle leaks (#783 added the compute facet, #784 eliminated most of
INativeHandlesContext). Three residual leaks survive that prevent true LSP — any non-Vulkan backend would still have to fake Vulkan's memory layout.Evidence (all verified at HEAD)
Leak 1 —
lpv_backend.zig:13downcasts*anyopaque→*VulkanContextA high-level lighting module directly names
VulkanContext(a backend-internal type) and downcasts the abstract context pointer. Any non-Vulkan backend would have to masquerade asVulkanContext.Leak 2 —
ComputeBuffer/ComputePipelinecarry raw Vulkan-shapedu64fieldsmodules/engine-rhi/src/rhi.zig:666-678:Then
rhi_vulkan.zig:1007-1201immediately casts them back:c.vkCmdBindPipeline(cmd, ..., @ptrFromInt(pipeline)),c.vkCmdBindDescriptorSets(cmd, ..., @ptrFromInt(pipeline_layout), ...), etc.Leak 3 —
imgui_backend.zig:31-45consumes 6 native handles in a high-level UI moduleengine-ui(high level) directly constructs 6 distinct Vulkan handle types fromINativeHandlesContext. ImGui's Vulkan backend inherently needs these — but the consumption should live behind anIImGuiBackendshim inengine-graphics, not inengine-ui.Bonus leak —
RhiError.VulkanErrornames a specific backend in the abstract error setmodules/engine-rhi/src/rhi_types.zig:7:VulkanErrorsits beside abstract variants (OutOfMemory,GpuLost,SurfaceLost). Callers can't pattern-match meaningfully when the abstract type names a concrete backend.Fix
Decision required first (call it out in the PR description):
Assuming (b):
Promote LPV behind the RHI. Add
ILightingContext(or extendIEffectsContext) withdrawLPVGrid(params),uploadLPVData(data). Rewritelpv_backend.zigagainst the abstract facet. Delete the@ptrFromInt(rhi.nativeHandles().getBackendContext())downcast.Make
ComputeBuffer/ComputePipelineopaque. Change public fields tohandle: u32(matchingBufferHandle/TextureHandle/ShaderHandlediscipline). Move theVkBuffer/VkDeviceMemory/VkPipeline/etc. fields into a backend-internal struct (vulkan/compute_resources.zig). Updaterhi_vulkan.zig:1007-1201to look them up viacompute_resources.get(handle)instead of@ptrFromInt(pipeline).Move ImGui Vulkan consumption into
engine-graphics. Createmodules/engine-graphics/src/vulkan/imgui_vulkan_bridge.zigthat consumesINativeHandlesContextand constructsImGui_ImplVulkan_InitInfo.engine-ui/imgui_backend.zigcallsrhi.initImGuiBackend(impl_data)(a newIImGuiContextmethod). Delete the 6 native-handle reads fromengine-ui.Rename
RhiError.VulkanError→RhiError.BackendError(or remove and replace with backend-agnosticDeviceError). Update the ~10 callers inengine-graphics.(Optional, related) Migrate the
anyerror!paths atrhi.zig:369, 596, 615, 689, 1096, 1109, 1147, 1171to typedRhiError!so callers can pattern-match.Verification
nix develop --command zig build test(includes shader validation)nix develop --command zig build test -- --test-filter "RHI"(will exercise R2-4 if landed first)grep -rE "@ptrFromInt\(.*nativeHandles|@ptrCast.*VulkanContext" modules/should return zero matches outsidemodules/engine-graphics/src/vulkan/Constraints
refactor(engine-rhi): make ComputePipeline opaque,refactor(engine-graphics): move ImGui Vulkan bridge, etc.Notes
INativeHandlesContextfor sky/water/shadow) — both closed. This issue finishes the job.engine-rhi/src/root.zig//! doc, deleteINativeHandlesContext's abstraction veneer, rename toIVulkanNativeHandles, and close R2-3 as wontfix". That's a legitimate resolution.Tracking: #839