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
| Pass | Inputs | Outputs |
|---|
| GBuffer | scene (OptiX launch) | albedo, normal, depth, position, material_id |
| Shadow | position, normal, sun_dir | shadow_mask |
| AO | position, normal, depth | ao_map |
| GI | position, normal, albedo, shadow_mask | indirect_light |
| Denoise | color, albedo, normal | denoised_color |
| Tonemap | denoised_color | ldr_color |
| Composite | ldr_color, overlays, HUD | final_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
- Define
RenderPass base class and RenderGraph container — topological sort, cycle detection, buffer descriptor registry - Extract GBuffer pass — wrap existing OptiX launch in a
GBufferPass, output named buffers instead of ad-hoc CuPy arrays - Extract Shadow and AO passes — already computed in the kernel; split into separate launches or keep fused with GBuffer and expose as outputs
- Extract Denoise pass — wrap existing OptiX denoiser call
- Extract Tonemap pass — color stretching, gamma, exposure currently in
_apply_post_processing() - Extract Composite pass — overlay blending, HUD rendering, minimap
- Buffer lifetime analysis — track first-use/last-use per buffer, reuse allocations where lifetimes don't overlap
- Capability-gated pass skipping — query
get_capabilities(), wire fallback connections - 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()
Context
Roadmap Phase 2 item (see #57). The current render pipeline is a fixed sequence hardcoded in
_update_frame()andanalysis/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:
Proposed Passes
Design
Pass Interface
Graph
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
RenderPassbase class andRenderGraphcontainer — topological sort, cycle detection, buffer descriptor registryGBufferPass, output named buffers instead of ad-hoc CuPy arrays_apply_post_processing()get_capabilities(), wire fallback connectionsScope Boundaries
References
analysis/render.py(render()function),engine.py(_update_frame(),_apply_post_processing())engine.pydenoiser setuprtx.py:get_capabilities()