Follow-up to #73. The render graph framework (RenderPass, RenderGraph, CompiledGraph) landed in #76 but doesn't touch the actual render pipeline. This issue is about extracting the stages from render() and _update_frame() into concrete RenderPass subclasses.
Goal
Replace the hardcoded sequential pipeline in analysis/render.py and engine.py with render graph execution. Right now, adding or reordering passes means editing control flow by hand.
Passes to extract
Each pass wraps existing CUDA kernel calls. No new GPU code, just reorganizing.
| Pass |
Current location |
Inputs |
Outputs |
| GBufferPass |
_generate_perspective_rays() + optix.trace() (primary) |
scene, camera params |
primary_rays, primary_hits, albedo, instance_ids, primitive_ids |
| ShadowPass |
_generate_shadow_rays_from_hits() + optix.trace() (occlusion) |
primary_rays, primary_hits, sun params |
shadow_hits |
| AOPass |
AO sample loop + _generate_ao_rays() + _accumulate_ao/gi() |
primary_rays, primary_hits |
ao_factor, gi_color |
| ReflectionPass |
_generate_reflection_rays() + optix.trace() |
primary_rays, primary_hits, instance_ids |
reflection_hits |
| ShadePass |
_shade_terrain() |
all ray buffers, colormap, overlays |
color, albedo |
| DenoisePass |
denoise() from rtx.py |
color, albedo, normals |
denoised_color |
| EdgeOutlinePass |
_edge_outline() |
color, instance_ids |
color (in-place) |
| EDLPass |
_edl() |
color, depth |
color (in-place) |
| BloomPass |
_bloom() |
color |
color (in-place) |
| TonemapPass |
_tone_map_aces() |
color |
color (in-place) |
Implementation approach
- Start with
render() (the offline path) since it's simpler — no accumulation loop, no particle splatting, no async readback.
- Pass classes go in
rtxpy/render_passes.py.
- A
build_default_graph() factory builds the same pipeline the current code runs.
- Add
use_render_graph=False to render() so the graph path is opt-in while both paths coexist.
- Once the graph path matches existing output pixel-for-pixel, flip the default and remove the old code path.
- Adapt
_update_frame() in the viewer to the same graph, with particle splatting and accumulation as viewer-specific passes.
Watch out for
- In-place passes (EdgeOutline, EDL, Bloom, Tonemap) read and write the same buffer. The graph handles this (a pass can list a buffer in both inputs and outputs), but they have to run in the right order after shading.
- AO accumulation runs multiple samples per frame with progressive accumulation in
_update_frame(). Probably best modeled as a single AOPass that loops internally, not N separate pass executions.
- External state — camera params, sun direction, colormap LUTs, overlay textures aren't graph buffers. Pass them via
external_buffers or a config dict on the pass.
- Double allocation —
_RenderBuffers currently handles allocation, the graph's AllocationPlan will replace it. During the opt-in period, can't have both allocating at once.
Done when
Follow-up to #73. The render graph framework (
RenderPass,RenderGraph,CompiledGraph) landed in #76 but doesn't touch the actual render pipeline. This issue is about extracting the stages fromrender()and_update_frame()into concreteRenderPasssubclasses.Goal
Replace the hardcoded sequential pipeline in
analysis/render.pyandengine.pywith render graph execution. Right now, adding or reordering passes means editing control flow by hand.Passes to extract
Each pass wraps existing CUDA kernel calls. No new GPU code, just reorganizing.
_generate_perspective_rays()+optix.trace()(primary)primary_rays,primary_hits,albedo,instance_ids,primitive_ids_generate_shadow_rays_from_hits()+optix.trace()(occlusion)primary_rays,primary_hits, sun paramsshadow_hits_generate_ao_rays()+_accumulate_ao/gi()primary_rays,primary_hitsao_factor,gi_color_generate_reflection_rays()+optix.trace()primary_rays,primary_hits,instance_idsreflection_hits_shade_terrain()color,albedodenoise()fromrtx.pycolor,albedo, normalsdenoised_color_edge_outline()color,instance_idscolor(in-place)_edl()color, depthcolor(in-place)_bloom()colorcolor(in-place)_tone_map_aces()colorcolor(in-place)Implementation approach
render()(the offline path) since it's simpler — no accumulation loop, no particle splatting, no async readback.rtxpy/render_passes.py.build_default_graph()factory builds the same pipeline the current code runs.use_render_graph=Falsetorender()so the graph path is opt-in while both paths coexist._update_frame()in the viewer to the same graph, with particle splatting and accumulation as viewer-specific passes.Watch out for
_update_frame(). Probably best modeled as a single AOPass that loops internally, not N separate pass executions.external_buffersor a config dict on the pass._RenderBufferscurrently handles allocation, the graph'sAllocationPlanwill replace it. During the opt-in period, can't have both allocating at once.Done when
render(use_render_graph=True)produces identical output to the current path (pixel-level comparison)explore()works with the graph path