simvx.core.scene_target_graph

SceneTargetGraph: inter-scene render-target dependency scheduler.

design/render_parallelism_and_multi_gpu.md P1-2. Where RenderGraph (graphics package) orders named passes within one scene render, this graph orders whole scene render jobs, one per offscreen target (each SubViewport, and later each RenderView) plus the main scene, so a producer target renders before any job that samples it in the same frame.

Jobs register the way RenderGraph passes do: :meth:SceneTargetGraph.add then :meth:SceneTargetGraph.compile. Edges are texture-sampling dependencies, expressed two ways:

  • implicit: a job consumes bindless slot integers; the job that produces that slot must come first (mirrors RenderGraph’s named string resources, with bindless slots as the resource names);

  • explicit: after lists producer job names (the feeds_from hint and the main scene’s “after every offscreen target” barrier).

Both graphs share the one Kahn implementation in

mod:

simvx.core._topo_sort (P1-1, no fork). Cycle policy differs by design: an intra-scene pass cycle is a bug (RenderGraph raises), but an inter-scene cycle (mirror facing mirror) is legitimate, so compile never raises: Tarjan SCC drops the minimal back-edge(s) and reports them as

attr:

lagged_edges, each sampling the previous frame’s target, scoped to the cyclic boundary. Under serial per-target execution a lagged edge is automatically correct (the previous frame’s content is fully resident).

Compilation is fingerprint-cached: re-registering an identical job set (the steady-state frame) reuses the previously computed order without re-sorting. The sort therefore runs on a structure change (a target added/removed, a slot assigned, a sampling binding changed), not per frame. Consumed-slot detection stays with the caller (order_subviewports scans subtrees), because a sampling binding such as material.albedo_tex_index can change without bumping the tree’s structure version.

Dependency-light on purpose (imports nothing from simvx.graphics): the web backend orders its offscreen targets through the same code path.

Module Contents

Classes

SceneJob

One scene render job: an offscreen target (or the main scene).

SceneTargetGraph

Orders scene render jobs so producer targets render before consumers.

Data

API

simvx.core.scene_target_graph.log

‘getLogger(…)’

simvx.core.scene_target_graph.__all__

[‘SceneJob’, ‘SceneTargetGraph’]

class simvx.core.scene_target_graph.SceneJob(name: str, *, node: Any = None, produces: int = -1, consumes: collections.abc.Iterable[int] = (), after: collections.abc.Iterable[str] = ())[source]

One scene render job: an offscreen target (or the main scene).

Args: name: Unique job name (deterministic across frames for a stable structure, so the compile cache holds). node: The owning scene node (a SubViewport), or None for the main scene job. Carried through so :attr:SceneTargetGraph.order maps straight back to nodes. produces: The bindless slot this job renders into, or -1 when it publishes no sampleable target (the main scene, a first-frame target with no slot yet). consumes: Bindless slots sampled inside this job’s scene. Slots no registered job produces are ignored (dangling inputs are allowed, as in RenderGraph: the resource comes from outside the graph). after: Names of jobs that must render first, regardless of slots (explicit feeds_from hints; the main scene’s barrier). Unknown names are ignored.

Initialization

__slots__

(‘name’, ‘node’, ‘produces’, ‘consumes’, ‘after’)

__repr__() str[source]
class simvx.core.scene_target_graph.SceneTargetGraph[source]

Orders scene render jobs so producer targets render before consumers.

Usage (mirrors RenderGraph)::

graph = SceneTargetGraph()
graph.clear()                      # start a (re-)registration
graph.add(SceneJob("svp:12", node=svp, produces=12, consumes={13}))
graph.add(SceneJob("svp:13", node=other, produces=13))
graph.add(SceneJob("scene:main", after=("svp:12", "svp:13")))
graph.compile()                    # sorts only if the structure changed
for job in graph.order: ...
graph.lagged_edges                 # broken cycle edges, usually empty

Initialization

__slots__

(‘_jobs’, ‘_fingerprint’, ‘_order_idx’, ‘_lagged_idx’, ‘_compile_count’)

clear() None[source]

Drop registered jobs (the compiled order survives until the next compile).

add(job: simvx.core.scene_target_graph.SceneJob) None[source]

Register a job. Call before :meth:compile. Duplicate names are an error.

compile() bool[source]

Topologically sort the registered jobs; producers first, never raises.

Fingerprint-cached: when the registered jobs describe the same structure as the previous compile (same names, slots, sampled slots and explicit edges, in the same registration order), the cached order is remapped onto the current job objects without re-sorting. Returns True when a real sort ran, False on a cache hit.

property order: tuple[simvx.core.scene_target_graph.SceneJob, ...][source]

Compiled render order (producers first).

property lagged_edges: frozenset[tuple[simvx.core.scene_target_graph.SceneJob, simvx.core.scene_target_graph.SceneJob]][source]

(producer, consumer) edges broken to resolve cycles (1-frame lag).

property compile_count: int[source]

How many real sorts have run (cache hits excluded).

property jobs: tuple[simvx.core.scene_target_graph.SceneJob, ...][source]

Registered jobs, in registration order.