Source code for simvx.graphics.renderer.render_graph

"""RenderGraph — declarative pass scheduler.

Topologically sorts passes by declared input/output dependencies at compile() time.
Frame-time iteration is a tight loop over pre-computed tuples — no per-frame graph
traversal, no per-frame validation.
"""

from __future__ import annotations

import logging
from typing import Any

from .render_pass import FrameContext, RenderPass

__all__ = ["RenderGraph"]

log = logging.getLogger(__name__)


[docs] class RenderGraph: """Schedules render passes by declared dependencies. Usage:: graph = RenderGraph() graph.add(shadow_pass) graph.add(ssao_pass) graph.compile() # once at setup graph.record_pre_render(cmd, frame) # every frame graph.record_render(cmd, frame) # every frame """ __slots__ = ("_passes", "_pre_render_order", "_render_order") def __init__(self) -> None: self._passes: list[RenderPass] = [] self._pre_render_order: tuple[RenderPass, ...] = () self._render_order: tuple[RenderPass, ...] = ()
[docs] def add(self, pass_: RenderPass) -> None: """Register a pass. Call before compile().""" self._passes.append(pass_)
[docs] def compile(self) -> None: """Topologically sort passes by input/output dependencies. Called once. Validates no cycles, no dangling inputs, no duplicate outputs. Splits result into pre_render and render tuples. """ # Build output → producer mapping producers: dict[str, RenderPass] = {} for p in self._passes: for out in p.outputs: if out in producers: raise ValueError( f"Duplicate output '{out}': both '{producers[out].name}' " f"and '{p.name}' produce it" ) producers[out] = p # Build adjacency: pass → set of passes that must run before it deps: dict[str, set[str]] = {p.name: set() for p in self._passes} pass_by_name: dict[str, RenderPass] = {p.name: p for p in self._passes} for p in self._passes: for inp in p.inputs: if inp in producers: deps[p.name].add(producers[inp].name) # Dangling inputs are allowed (resource may come from outside the graph, # e.g. previous frame's depth buffer). # Kahn's algorithm (topological sort) in_degree = {name: len(d) for name, d in deps.items()} # Seed with zero-degree nodes, sorted by name for deterministic order queue = sorted(name for name, deg in in_degree.items() if deg == 0) ordered: list[str] = [] # Reverse adjacency for Kahn's dependents: dict[str, list[str]] = {name: [] for name in deps} for name, dep_set in deps.items(): for dep_name in dep_set: dependents[dep_name].append(name) while queue: node = queue.pop(0) ordered.append(node) # Sort dependents by name for deterministic tiebreaking for dep in sorted(dependents[node]): in_degree[dep] -= 1 if in_degree[dep] == 0: queue.append(dep) queue.sort() # maintain sorted order if len(ordered) != len(self._passes): missing = set(deps) - set(ordered) raise ValueError(f"Cycle detected in render graph involving: {missing}") # Split into pre_render and render tuples pre = [] ren = [] for name in ordered: p = pass_by_name[name] if p.stage == "pre_render": pre.append(p) else: ren.append(p) self._pre_render_order = tuple(pre) self._render_order = tuple(ren) log.debug( "Render graph compiled: pre_render=[%s], render=[%s]", ", ".join(p.name for p in self._pre_render_order), ", ".join(p.name for p in self._render_order), )
[docs] def record_pre_render(self, cmd: Any, frame: FrameContext) -> None: """Record all pre-render passes in dependency order.""" for p in self._pre_render_order: if p.enabled: p.record(cmd, frame)
[docs] def record_render(self, cmd: Any, frame: FrameContext) -> None: """Record all render-stage passes in dependency order.""" for p in self._render_order: if p.enabled: p.record(cmd, frame)
[docs] def resize(self, width: int, height: int) -> None: """Propagate resize to all passes.""" for p in self._passes: p.resize(width, height)
[docs] def destroy(self) -> None: """Destroy all passes.""" for p in self._passes: p.destroy()
@property def passes(self) -> list[RenderPass]: """All registered passes (unordered).""" return self._passes @property def pre_render_order(self) -> tuple[RenderPass, ...]: """Compiled pre-render execution order.""" return self._pre_render_order @property def render_order(self) -> tuple[RenderPass, ...]: """Compiled render execution order.""" return self._render_order