simvx.core._subviewport_order

Backend-agnostic per-frame ordering of SubViewport render targets.

A :class:~simvx.core.SubViewport renders its subtree into an offscreen bindless texture each frame. When SubViewport A samples SubViewport B’s texture (material.albedo_tex_index = B.texture or sprite._texture_id = B.texture), 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_subviewports is the single canonical implementation both the desktop (Vulkan) and web (WebGPU) backends call. It:

  1. Builds {slot -> producer} from each live SubViewport’s current bindless slot (node._texture_id).

  2. Scans each SubViewport’s own subtree (stopping at nested SubViewport boundaries) for any material / Sprite-like sampler slot equal to another live SubViewport’s slot, and unions the explicit node.feeds_from hints, to build producer -> consumer edges.

  3. Kahn topo-sorts so producers precede consumers (deterministic tiebreak by discovery index).

  4. On a genuine cycle (e.g. two mirrors facing each other) 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

scan_consumed_slots

Collect every bindless slot consumed inside viewport’s subtree.

order_subviewports

Order live SubViewports 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 (MeshInstance3D and 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], *, depth_cap: int = 1) tuple[list[Any], set[tuple[Any, Any]]][source]

Order live SubViewports so producers render before consumers.

Args: live: SubViewport nodes in discovery order (the flat DFS order the backend already collects). slot_of: node -> int returning the node’s current bindless slot (node._texture_id); < 0 means “no slot yet” (first frame). depth_cap: Bound on recursive scene-feedback depth (the WorldEnvironment.subviewport_feedback_depth property). The default 1 renders each SubViewport once per frame; cyclic back-edges then lag by one frame. depth_cap <= 0 is treated as 1 (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.

Returns: (ordered, lagged_edges) where ordered is the list of the same SubViewport nodes in render order, and lagged_edges is 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.