simvx.core._subviewport_order¶
Backend-agnostic per-frame ordering of offscreen scene render targets.
A :class:~simvx.core.SubViewport renders its subtree, and a
- class:
~simvx.core.RenderViewrenders the main scene, into an offscreen bindless texture each frame. When target A samples target B’s texture (material.albedo_tex_index = B.texture, or aSprite2Dwhosetextureis B), B is a producer and A a consumer: B must render first so A sees fresh content the same frame instead of last frame’s (the 1-frame lag that flat discovery order causes).- func:
order_subviewportsis the single canonical implementation both the desktop (Vulkan) and web (WebGPU) backends call, for SubViewports alone or for a mixed SubViewport + RenderView list (viaconsumes_of). It:
Builds one :class:
~simvx.core.scene_target_graph.SceneJobper live target (produces= the node’s current bindless slot,node._texture_id) plus ascene:mainjob that renders after every offscreen target.Computes each job’s consumed slots (the default scans the SubViewport’s own subtree, stopping at nested SubViewport boundaries; a RenderView consumes what the main tree samples, supplied through
consumes_of), and carries the explicitnode.feeds_fromhints asafteredges.Compiles the jobs through :class:
SceneTargetGraph, which Kahn topo-sorts so producers precede consumers (deterministic tiebreak by discovery index) via the shared :func:simvx.core._topo_sort.topo_sort_laggedhelper.On a genuine cycle (e.g. two mirrors facing each other) the graph runs Tarjan SCC and breaks the minimal back-edge(s) deterministically, so only the cyclic boundary lags (the broken consumer reads last frame’s texture, the natural result of rendering it before its producer). It never raises.
First-frame warmup. Before the backend has assigned slots, every
slot_of(node) returns -1; no edges form, so the order is the flat
discovery order. That is correct: there is nothing to sample yet. From the
second frame on (slots assigned) the topological order takes effect, so a
producer -> consumer chain is lag-free from frame 1 of actually sampling.
The function is dependency-light: it imports nothing from simvx.graphics
and inspects only core data (Material, Sprite-like _texture_id).
Module Contents¶
Functions¶
Collect every bindless slot consumed inside viewport’s subtree. |
|
Order live offscreen targets so producers render before consumers. |
Data¶
API¶
- simvx.core._subviewport_order.log¶
‘getLogger(…)’
- simvx.core._subviewport_order.__all__¶
[‘order_subviewports’, ‘scan_consumed_slots’]
- simvx.core._subviewport_order.scan_consumed_slots(viewport: Any) set[int][source]¶
Collect every bindless slot consumed inside viewport’s subtree.
Walks the SubViewport’s children in DFS, stopping at nested SubViewport boundaries (a nested SubViewport’s own consumption is its own edge, scanned when that nested node is processed from the live list, so each edge is counted once). For every node it reads:
node.material.albedo_tex_index(MeshInstance3Dand friends);node._texture_id(Sprite2D / Sprite3D / MeshInstance2D / NinePatch / AnimatedSprite2D, whose drawn texture slot lives here).
Returns the set of slot integers found (negatives included; the caller filters them against the live-producer map). Excludes viewport’s own published slot, which it does not consume.
- simvx.core._subviewport_order.order_subviewports(live: list[Any], slot_of: collections.abc.Callable[[Any], int], *, consumes_of: collections.abc.Callable[[Any], set[int]] | None = None, depth_cap: int = 1, graph: simvx.core.scene_target_graph.SceneTargetGraph | None = None) tuple[list[Any], set[tuple[Any, Any]]][source]¶
Order live offscreen targets so producers render before consumers.
The order is produced by a :class:
SceneTargetGraphover one- Class:
SceneJobper target plus ascene:mainsink job (the main scene renders after every offscreen target, which is exactly the engine’s pre-render/main-pass split).
Args: live: SubViewport (and optionally RenderView) nodes in discovery order (the flat DFS order the backend already collects). slot_of:
node -> intreturning the node’s current bindless slot (node._texture_id);< 0means “no slot yet” (first frame). consumes_of:node -> set[int]returning the bindless slots the node’s render job samples. Defaults to :func:scan_consumed_slots(the SubViewport own-subtree scan). A mixed-target caller supplies a dispatcher that hands RenderView jobs the main tree’s consumed slots (a RenderView renders the main scene, so it samples whatever the main pass samples), memoised per frame. depth_cap: Bound on recursive scene-feedback depth (theWorldEnvironment.scene_feedback_max_depthproperty). The default1renders each SubViewport once per frame; cyclic back-edges then lag by one frame.depth_cap <= 0is treated as1(each node still renders once). Higher caps are reserved for a future multi-pass feedback expansion and currently behave like 1 for the ordering itself (each node appears once); the cap is honoured in that no node is scheduled more times than the cap allows. graph: Optional persistent :class:SceneTargetGraph. Passing the same instance every frame engages its compile cache, so the topo-sort runs only when the structure changes (a viewport added/removed, a slot assigned, a sampling binding changed), not per frame.Nonebuilds a throwaway graph (identical result, sorts every call).Returns:
(ordered, lagged_edges)whereorderedis the list of the same SubViewport nodes in render order, andlagged_edgesis the set of(producer, consumer)edges that were broken to resolve a cycle (the consumer reads last frame’s texture across that edge). Empty on the acyclic common case.Never raises on a cyclic graph: cycles degrade to a 1-frame-lagged edge.