Skip to content

Extract render passes into RenderPass subclasses #77

@brendancol

Description

@brendancol

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

  1. Start with render() (the offline path) since it's simpler — no accumulation loop, no particle splatting, no async readback.
  2. Pass classes go in rtxpy/render_passes.py.
  3. A build_default_graph() factory builds the same pipeline the current code runs.
  4. Add use_render_graph=False to render() so the graph path is opt-in while both paths coexist.
  5. Once the graph path matches existing output pixel-for-pixel, flip the default and remove the old code path.
  6. 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

  • render(use_render_graph=True) produces identical output to the current path (pixel-level comparison)
  • explore() works with the graph path
  • A custom pass can be inserted into the default graph (e.g. a user post-process)
  • Disabling a pass via the graph (e.g. AO, denoise) matches disabling via render parameters

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