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
class RenderPass:
name: str
inputs: dict[str, BufferDesc] # name → (dtype, channels)
outputs: dict[str, BufferDesc]
enabled: bool = True
def setup(self, graph: RenderGraph) -> None: ...
def execute(self, buffers: dict[str, DeviceBuffer]) -> None: ...
def teardown(self) -> None: ...
Graph
class RenderGraph:
def add_pass(self, pass_: RenderPass) -> None: ...
def remove_pass(self, name: str) -> None: ...
def compile(self) -> list[RenderPass]:
"""Topological sort, buffer lifetime analysis, allocation plan."""
def execute(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()