-
Notifications
You must be signed in to change notification settings - Fork 3
Open
Labels
enhancementNew feature or requestNew feature or request
Description
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
RenderPassbase class andRenderGraphcontainer — 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.pydenoiser setup - Capability detection:
rtx.py:get_capabilities()
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request