Skip to content

[R2-3][P1] Resolve residual RHI native-handle leak (lpv_backend, ComputePipeline, imgui) #842

Description

@MichaelFisher1997

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):

  1. 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.

  2. 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).

  3. 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.

  4. Rename RhiError.VulkanErrorRhiError.BackendError (or remove and replace with backend-agnostic DeviceError). Update the ~10 callers in engine-graphics.

  5. (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

Metadata

Metadata

Assignees

No one assigned

    Labels

    automated-auditIssues found by automated opencode audit scansbugSomething isn't workingdocumentationImprovements or additions to documentationengineenhancementNew feature or requesthotfix

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions