simvx.graphics.renderer.sub_viewport

SubViewport render-to-texture: offscreen targets for core.SubViewport nodes.

A :class:~simvx.core.SubViewport renders its own subtree (with its own camera) into an offscreen texture that other nodes in the main scene can sample: a “live monitor in the world”, a security-camera feed, a minimap, a render-to-texture portal, etc.

Architecture mirrors :class:~simvx.graphics.renderer.game_viewport.GameViewportRenderer (the proven editor game-preview path): each SubViewport owns a

class:

RenderTarget, registered once as a bindless texture whose slot id stays stable across resizes so consumers that captured subviewport.texture keep working. :class:SubViewportManager keys one renderer per SubViewport by id(node), creates it on first sight, debounce-resizes on size changes, and unregisters on exit-tree / teardown.

Frame ordering: all SubViewports are recorded before the main scene pass, in

meth:

SubViewportManager.render_all (driven from the engine pre_render callback) into the same primary command buffer the whole frame uses. Each is its own scene-render unit (SRU): it reserves a slice of the shared transform SSBO and records its draws with absolute first_instance indices, so a single vkQueueSubmit covers every SubViewport plus the main scene with no per-viewport vkQueueWaitIdle. Each offscreen pass ends in SHADER_READ_ONLY (the render pass’s outgoing colour dependency makes the write visible to a later sample in the same command buffer).

The per-frame render list is topologically ordered by the shared core helper :func:simvx.core._subviewport_order.order_subviewports: when one SubViewport samples another’s offscreen texture, the producer renders first, so the consumer sees this frame’s content (no lag). The producer’s offscreen pass ends in SHADER_READ_ONLY and its render pass’s outgoing colour dependency already makes that write visible to the later same-cmd sample, so no extra inter-SubViewport barrier is needed. Genuine cycles (mirror facing mirror) degrade gracefully: the minimal back-edge is broken and only that one consumer lags a frame; the helper never raises. The recursion depth is bounded by WorldEnvironment.scene_feedback_max_depth (default 1 = render each once).

Module Contents

Classes

SubViewportRenderer

One offscreen render target for a single SubViewport node.

SubViewportManager

Owns one :class:SubViewportRenderer per live SubViewport node.

Data

API

simvx.graphics.renderer.sub_viewport.log

‘getLogger(…)’

simvx.graphics.renderer.sub_viewport.__all__

[‘SubViewportRenderer’, ‘SubViewportManager’]

class simvx.graphics.renderer.sub_viewport.SubViewportRenderer(engine: Any)[source]

One offscreen render target for a single SubViewport node.

Wraps a :class:RenderTarget and exposes the ready / width / height / begin_pass / end_pass / render_draw2d surface that

Meth:

SceneAdapter.render_to_target expects: the same contract

Class:

GameViewportRenderer satisfies.

Initialization

create(width: int, height: int) None[source]

Create the offscreen target and register its colour view as bindless.

resize(width: int, height: int) None[source]

Recreate the target at a new size, preserving the bindless slot.

The slot id handed out as subviewport.texture stays stable; only the backing image view changes, so a Sprite2D / Material that captured the slot keeps sampling the live feed after a resize.

ensure_gbuffer() None[source]

Recreate the target if the engine’s thin-G-buffer activation changed.

SSR/SSGI toggling the G-buffer rebuilds the forward pipelines with a second colour output; a single-attachment offscreen pass would then be render-pass-incompatible with them. Called from the manager’s prepare phase (before any offscreen draw is recorded), it rebuilds the target to match, preserving the bindless slot. A no-op on the common path.

begin_pass(cmd: Any) None[source]

Begin the offscreen colour+depth pass (clear).

end_pass(cmd: Any) None[source]
render_draw2d(cmd: Any, ops: list) None[source]

Overlay pre-extracted Draw2D ops on top of the 3D content (LOAD_OP_LOAD).

render_items(cmd: Any, view: Any, camera: Any) None[source]

Render a published 2D item view into this target (RTT-2D).

The item-pipeline counterpart of :meth:render_draw2d: the SubViewport’s own 2D subtree is collected + published (with the viewport’s own Camera2D affine, camera) on the game thread, and this submits that frozen

Class:

~simvx.graphics.render2d.publish.PublishedItemView into the offscreen colour target through the existing 2D pipelines. It reuses the colour-only overlay_render_pass (LOAD_OP_LOAD, no depth): 2D needs no depth attachment, which resolves the 2D-only target depth-attachment incompatibility: the SubViewport keeps its depth attachment for 3D content, and the 2D overlay pass simply ignores it.

The submit is render-target-agnostic: same collection / sort / batch as the main framebuffer; only the target framebuffer + the per-target Camera2D differ. Text2D flows through as first-class GLYPH items (the legacy render_draw2d path never saw Text2D because its on_draw only emits through the item builder).

property texture_id: int[source]
property width: int[source]
property height: int[source]
property ready: bool[source]
destroy() None[source]

Release the target, Draw2D pass, and the bindless slot.

class simvx.graphics.renderer.sub_viewport.SubViewportManager(engine: Any, adapter: Any)[source]

Owns one :class:SubViewportRenderer per live SubViewport node.

Created once per :class:App run and invoked from the engine pre_render callback via :meth:render_all. Discovers SubViewports each frame by walking the main scene tree, lazily creating a target on first sight, debounce-resizing when SubViewport.size changes, and reaping targets whose nodes have left the tree.

Initialization

render_all(cmd: Any, tree: Any) bool[source]

Record every SubViewport in tree into the frame’s primary command buffer.

Called from the engine pre_render callback, before the main scene pass, with the frame’s primary cmd. Each SubViewport renders its own subtree with its own camera as a scene-render unit (SRU): it reserves a slice of the shared transform SSBO and records its draws with absolute first_instance indices into cmd. No per-viewport command buffer, no vkQueueWaitIdle: the whole frame ends in one vkQueueSubmit.

Each offscreen pass leaves its colour target in SHADER_READ_ONLY and the offscreen render pass’s outgoing colour dependency makes that write visible to a later sample in the same command buffer.

The list is topologically ordered so a producer renders before any SubViewport that samples it (see :meth:_order_live); a consumer therefore sees this frame’s content. Genuine cycles degrade to a one-frame-lagged back-edge and never raise.

Returns True if at least one SubViewport rendered this frame (purely informational now: there is no longer a re-submit, since the main scene’s submission lists are preserved across each SRU).

prepare_all(tree: Any) list[tuple[Any, simvx.graphics.renderer.sub_viewport.SubViewportRenderer, simvx.graphics.renderer.sub_viewport._SubTreeView, Any, Any]][source]

Pass 1 of :meth:render_all: create/resize/publish, NO cmd recording.

Discovers + topologically orders the live SubViewports, creates or debounce-resizes each renderer, honours render_target_update_mode, publishes each viewport’s 2D items, and plans multi-GPU offload. All bindless-texture descriptor updates (register_texture on create, update_texture on resize) happen HERE, so a caller can interleave the returned entries’ recording with other offscreen targets (RenderViews) without mutating a descriptor set an already-recorded draw bound.

Returns the ordered entries for :meth:record_one; empty when nothing renders this frame.

record_one(cmd: Any, entry: tuple[Any, simvx.graphics.renderer.sub_viewport.SubViewportRenderer, simvx.graphics.renderer.sub_viewport._SubTreeView, Any, Any]) None[source]

Pass 2 of :meth:render_all: record ONE prepared SubViewport SRU.

No descriptor mutation happens here, so binds recorded for an earlier offscreen target stay valid. Each SRU reserves its own transform-SSBO slice and records absolute first_instance draws; the main scene’s base-0 slice is untouched. sru_id = id(node) keeps the visibility cache from colliding.

build_srus(tree: Any) list[source]

Snapshot each live SubViewport into an owned :class:SubViewportSRU plan.

The pipelined-extract counterpart of :meth:render_all: same discovery, topological ordering, create/resize, and update-mode gating, but instead of recording into a command buffer it captures, on the MAIN thread, the owned inputs the render thread needs to record the SRU offscreen WITHOUT walking the live tree, the submission lists, camera matrices, clear colour, and isolated Draw2D ops.

Returns the plans producer-first (so a consumer SRU follows the producer it samples). Empty when no SubViewport is present. Create/resize + bindless descriptor updates still happen here (on the main thread, before the render thread records), exactly as in render_all pass 1.

destroy() None[source]

Tear down all targets (call on app shutdown).