Skip to content

Render graph: configurable DAG of render passes #73

Description

@brendancol

Context

Roadmap Phase 2 item (see #57). The current render pipeline is a fixed sequence hardcoded in _update_frame() and analysis/render.py. Adding or removing passes (AO, denoiser, DOF) requires editing control flow and managing buffer dependencies manually. A render graph makes the pipeline declarative and extensible.

Goal

A lightweight DAG of render passes that:

  • Declares inputs/outputs per pass (buffer names + formats)
  • Auto-resolves execution order from data dependencies
  • Allocates/reuses GPU buffers based on lifetime analysis
  • Makes it trivial to add, remove, or reorder passes without touching other passes

Proposed Passes

GBuffer → Shadow → AO → GI → Denoise → Tonemap → Composite
PassInputsOutputs
GBufferscene (OptiX launch)albedo, normal, depth, position, material_id
Shadowposition, normal, sun_dirshadow_mask
AOposition, normal, depthao_map
GIposition, normal, albedo, shadow_maskindirect_light
Denoisecolor, albedo, normaldenoised_color
Tonemapdenoised_colorldr_color
Compositeldr_color, overlays, HUDfinal_framebuffer

Design

Pass Interface

classRenderPass:
name: strinputs: dict[str, BufferDesc] # name → (dtype, channels)outputs: dict[str, BufferDesc]
enabled: bool=Truedefsetup(self, graph: RenderGraph) ->None: ...
defexecute(self, buffers: dict[str, DeviceBuffer]) ->None: ...
defteardown(self) ->None: ...

Graph

classRenderGraph:
defadd_pass(self, pass_: RenderPass) ->None: ...
defremove_pass(self, name: str) ->None: ...
defcompile(self) ->list[RenderPass]:
"""Topological sort, buffer lifetime analysis, allocation plan."""defexecute(self) ->None:
"""Run compiled pass order, managing buffers."""

Capability Gating

Passes declare required capabilities (e.g., requires=["optix_denoiser"]). The graph skips passes whose requirements aren't met, with fallback wiring (e.g., if Denoise is skipped, Tonemap reads raw color instead of denoised_color).

Implementation Plan

  1. Define RenderPass base class and RenderGraph container — topological sort, cycle detection, buffer descriptor registry
  2. Extract GBuffer pass — wrap existing OptiX launch in a GBufferPass, output named buffers instead of ad-hoc CuPy arrays
  3. Extract Shadow and AO passes — already computed in the kernel; split into separate launches or keep fused with GBuffer and expose as outputs
  4. Extract Denoise pass — wrap existing OptiX denoiser call
  5. Extract Tonemap pass — color stretching, gamma, exposure currently in _apply_post_processing()
  6. Extract Composite pass — overlay blending, HUD rendering, minimap
  7. Buffer lifetime analysis — track first-use/last-use per buffer, reuse allocations where lifetimes don't overlap
  8. Capability-gated pass skipping — query get_capabilities(), wire fallback connections
  9. Validation — detect missing inputs, warn on unused outputs, cycle detection

Scope Boundaries

  • No multi-GPU pass distribution (single device)
  • No async pass overlap initially (sequential execution, async is a future optimization)
  • GBuffer pass may remain fused (single OptiX launch producing multiple outputs) rather than separate geometry/material passes — splitting the kernel is not worth the extra launch overhead at this stage

References

  • Current render pipeline: analysis/render.py (render() function), engine.py (_update_frame(), _apply_post_processing())
  • OptiX denoiser integration: engine.py denoiser setup
  • Capability detection: rtx.py:get_capabilities()

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions