simvx.graphics.renderer.render_packet

Field provenance

Every field mirrors a per-frame attribute the GPU frame reads, each cleared in Renderer.begin_frame (forward.py ~605-616) and rebuilt by the scene adapter:

instances Renderer._instances (forward.py ~64): list of (MeshHandle, transform, material_id, viewport_id). Consumed by _upload_transforms, the shadow pass, occlusion cull, and the forward draw. The 4x4 transform arrays are copied so the main thread may rebuild next frame without touching the in-flight packet. skinned_instances Renderer._skinned_instances (forward.py ~176): (MeshHandle, transform, material_id, joint_matrices). Both numpy arrays per entry are copied. shader_material_submissions Renderer._shader_material_submissions (forward.py ~120): (MeshHandle, transform, material_id, shader_material, texture_slots). The transform and the resolved texture-slot mapping are copied; shader_material is a shared, frame-stable object referenced. particle_submissions Renderer._particle_submissions (forward.py ~109): (data, count). data is copied. gpu_particle_submissions Renderer._gpu_particle_submissions (forward.py ~113): (emitter_id, emitter_config). emitter_config is a per-frame dict; shallow-copied so a downstream mutation of the dict object is isolated. materials Renderer._materials (forward.py ~105, set via set_materials). Copied. lights Renderer._lights (forward.py ~106, set via set_lights). Copied. viewports Snapshot of Renderer.viewport_manager.viewports (scene_adapter ~785). Each entry is a :class:ViewportSnapshot carrying the viewport id plus an owned copy of the camera view/proj matrices, rect, and the (shared, not copied) render-target handle. structure_version tree._structure_version at extract time. The velocity and occlusion passes guard prev<->cur pairing on this; the render thread must see the value that matched the snapshotted instance ordering, not a later one. frame_index Monotonic producer frame counter, for ordering / telemetry / debugging. item_view Published, immutable 2D item-pipeline view: the frozen SoA item columns the render thread reads instead of the live game-thread item store. None when the scene drew no 2D this frame. A clean frame republishes the SAME view object (identity-equal, zero-byte) – the double-buffer reuse contract, achieved via an immutable frozen view rather than a per-frame re-copy.

Subsystem submission buffers (packetised)

These mirror per-frame submission lists that live inside lazily-created subsystem passes. Each is rebuilt every frame on the MAIN thread (during adapter.submit_scene) and read during recording, so the render thread must read an OWNED copy rather than the live list the main thread is clearing.

tilemap_layers Owned snapshot of Renderer._tilemap_pass._submissions (tilemap_pass.py ~42, populated by submit_layer from SceneAdapter._submit_tilemaps). Each entry is (tile_data: ndarray, tileset_texture_id: int, tile_size: tuple,     colours: ndarray | None); the tile_data structured array (and the optional colours tint array, when present) are copied (the main thread reuses TileMap layer buffers across frames). light2d_lights / light2d_occluders Owned snapshots of Renderer._light2d_pass._lights (list of per-light dicts, light2d_pass.py ~65) and ._occluders (list of polygon vertex-lists, ~66). Each dict / polygon is shallow-copied so a downstream mutation of the per-frame entry is isolated from the packet.

(Text2D no longer has a dedicated overlay geometry buffer to snapshot: after
the unified text path draws through the ``item_view``, which the
``item_view`` packet field already carries, so there is no separate text
snapshot here.)

Scene-render-unit (SRU) plans

subviewport_srus Ordered list of :class:SubViewportSRU plans, one per live SubViewport, captured on the MAIN thread by :meth:SubViewportManager.build_srus (which reuses the topological order_subviewports so producers precede consumers). Each plan owns the SubViewport’s submitted opaque/skinned instances (transform copies), its camera view/proj matrices, its published 2D item view, target identity, and the update-mode decision, so the render thread can record the offscreen SRU WITHOUT walking the live tree. Empty when no SubViewport is present or in the synchronous path (which still walks the tree live, byte-identically).

State intentionally NOT snapshotted (and why)

  • _main_base / arena slice info: reservation is a GPU-SSBO concern owned entirely by the render thread (reserve_main_slice + _upload_transforms run there from the packet’s instances). The main thread never reserves.

  • Per-frame HDR / post flags (_hdr_rendered): recomputed by the render thread inside pre_render/render from the snapshotted lists; they are outputs of recording, not inputs to it.

  • Reflection-probe capture (ReflectionProbePass.update_probes): DEFERRED. The probe path is deeply GPU-stateful (per-probe source cubemaps, six face renders, an IBL compute convolution with record-time descriptor mutation, and cube-array copies with many intra-cmd barriers). Packetising it faithfully would require snapshotting six face instance-lists per probe plus replaying the convolution/copy state machine on the render thread, which is out of scope here. Probe-bearing scenes keep the safe skip + one-time warning in pipelined mode (app.py _warn_pipelined_unpacketised / pre_render_fn).

  • RenderView capture (RenderViewManager): DEFERRED on the same precedent. A RenderView re-renders the live MAIN tree through a second camera; packetising it faithfully would mean snapshotting a full extra main-scene instance list per view each frame. In pipelined mode the manager is simply not invoked (frame_loop pre_render takes the packet-replay branch): the published slot stays valid but stale, covered by the same one-time warning. Immediate (synchronous) mode is the supported path.

Note: the 2D HUD/overlay (immediate-mode + Text2D) is carried by the published item_view (installed via install_packet), so the render thread never touches the live game-thread item store. The subsystem buffers (tilemap / 2D-light / 3D-overlay text) and SubViewport SRUs documented above are now packet-owned AND consumed by the render thread: Renderer.install_packet binds them onto the per-frame override attributes the pass read-sites consult, and the pipelined pre_render replays the SubViewport SRUs from the plan via SceneAdapter.render_sru_from_plan (no live-tree walk). Only reflection-probe and RenderView capture remain deferred in pipelined mode (see above). The synchronous path installs no packet, so every read-site falls back to the live state and stays byte-identical.

CPU-side per-frame render snapshot for the pipelined render thread.

A :class:RenderPacket is an owned, immutable-by-convention snapshot of the renderer’s per-frame submission state, taken on the MAIN thread right after SceneAdapter.submit_scene populates that state. In pipelined render mode the packet is handed to the render thread, which reconstructs the renderer’s per-frame lists from it and then records + submits the GPU frame, while the main thread is free to simulate the next frame and rebuild the renderer’s (now empty) lists without mutating the in-flight packet.

The packet, its :func:extract_render_packet builder, and the

class:

RenderPacketRing double-buffer are defined and unit tested. The default (synchronous) path never constructs a packet, so it stays byte-identical.

Module Contents

Classes

ViewportSnapshot

Owned snapshot of one viewport’s camera + rect for a single frame.

SubViewportSRU

Owned plan to record one SubViewport offscreen without walking the tree.

RenderPacket

Owned snapshot of the renderer’s per-frame submission state.

RenderPacketRing

Bounded double-buffered handoff between the main and render threads.

Functions

extract_render_packet

Snapshot the renderer’s current per-frame state into an owned RenderPacket.

Data

API

simvx.graphics.renderer.render_packet.__all__

[‘RenderPacket’, ‘RenderPacketRing’, ‘SubViewportSRU’, ‘ViewportSnapshot’, ‘extract_render_packet’]

class simvx.graphics.renderer.render_packet.ViewportSnapshot

Owned snapshot of one viewport’s camera + rect for a single frame.

Matrices are copied so the render thread reads a stable view/proj even after the main thread rebuilds the live Viewport next frame (TAA jitter and the motion-blur matrix update both mutate camera_proj in place during recording, which must happen on the render thread’s owned copy).

vp_id: int

None

x: int

None

y: int

None

width: int

None

height: int

None

camera_view: numpy.ndarray

None

camera_proj: numpy.ndarray

None

render_target: Any | None

None

class simvx.graphics.renderer.render_packet.SubViewportSRU

Owned plan to record one SubViewport offscreen without walking the tree.

Captured on the MAIN thread by :meth:SubViewportManager.build_srus from a SubViewport’s already-submitted offscreen scene. The render thread replays it: reserve a transform-SSBO slice for instances + skinned_instances, write those transforms, record the draws into the SubViewport’s renderer target with camera_view / camera_proj, then overlay draw2d_ops. The producer-before-consumer ordering is encoded by this list’s position in :attr:RenderPacket.subviewport_srus (topological sort), so a consumer SRU appears after the producer it samples.

Fields

sru_id Stable identity (id(node)) keying the frustum-visibility cache so SRUs never collide. Mirrors render_to_target(sru_id=...). renderer The SubViewport’s :class:SubViewportRenderer (shared GPU target, not copied: it owns the offscreen image whose bindless slot the main scene samples). The render thread records into it; the main thread does not mutate it concurrently (create/resize happen during extract, before the plan is built). width / height The SRU’s offscreen extent at capture time (drives viewport + the SSBO transform write). clear_colour Per-frame clear colour (transparent_bg decides RGBA), copied. camera_view / camera_proj Owned copies of the SRU camera’s view + projection matrices (None for a 2D-only SubViewport, which uses the screen-size path). screen_size (width, height) float screen size override the 2D path needs. instances / skinned_instances Owned snapshots of the offscreen scene’s submitted opaque / skinned instances (same shape as :attr:RenderPacket.instances / skinned_instances; transform + joint arrays copied). billboard_submissions Owned snapshot of the subtree’s Sprite3D / Text3D billboard rows so the render thread renders them into the offscreen target (parity with the synchronous render_to_target path). Empty when the subtree has none. item_view The published, immutable 2D item-pipeline view for THIS SubViewport target: each SubViewport SRU snapshots the published item view for its target. None when the subtree drew no 2D this frame.

sru_id: int

None

renderer: Any

None

width: int

None

height: int

None

clear_colour: tuple[float, float, float, float]

None

camera_view: numpy.ndarray | None

None

camera_proj: numpy.ndarray | None

None

screen_size: tuple[float, float]

None

instances: list[tuple[simvx.graphics.types.MeshHandle, numpy.ndarray, int, int]]

None

skinned_instances: list[tuple[simvx.graphics.types.MeshHandle, numpy.ndarray, int, numpy.ndarray]]

None

multimesh_blocks: list[tuple[simvx.graphics.types.MeshHandle, numpy.ndarray, int, numpy.ndarray | None, int, int, int]]

‘field(…)’

billboard_submissions: list[numpy.ndarray]

‘field(…)’

item_view: simvx.graphics.render2d.publish.PublishedItemView | None

None

item_camera: tuple[float, float, float, float, float, float]

(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)

class simvx.graphics.renderer.render_packet.RenderPacket

Owned snapshot of the renderer’s per-frame submission state.

Construct via :func:extract_render_packet. All mutable numpy data is copied at extract time, so the main thread may rebuild the renderer’s per-frame lists for the next frame while this packet is in flight on the render thread.

frame_index: int

None

structure_version: int

None

instances: list[tuple[simvx.graphics.types.MeshHandle, numpy.ndarray, int, int]]

None

skinned_instances: list[tuple[simvx.graphics.types.MeshHandle, numpy.ndarray, int, numpy.ndarray]]

None

shader_material_submissions: list[tuple[simvx.graphics.types.MeshHandle, numpy.ndarray, int, Any, dict[str, int]]]

None

particle_submissions: list[tuple[numpy.ndarray, int]]

None

gpu_particle_submissions: list[tuple[int, dict]]

None

materials: numpy.ndarray

None

lights: numpy.ndarray

None

viewports: list[simvx.graphics.renderer.render_packet.ViewportSnapshot]

‘field(…)’

tilemap_layers: list[tuple[numpy.ndarray, int, tuple[float, float], numpy.ndarray | None]]

‘field(…)’

billboard_submissions: list[numpy.ndarray]

‘field(…)’

light2d_lights: list[dict]

‘field(…)’

light2d_occluders: list[list[tuple[float, float]]]

‘field(…)’

camera2d_affine: tuple[tuple[float, ...], tuple[float, float]] | None

None

subviewport_srus: list[simvx.graphics.renderer.render_packet.SubViewportSRU]

‘field(…)’

multimesh_blocks: list[tuple[simvx.graphics.types.MeshHandle, numpy.ndarray, int, numpy.ndarray | None, int, int, int]]

‘field(…)’

item_view: simvx.graphics.render2d.publish.PublishedItemView | None

None

item_camera: tuple

(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)

simvx.graphics.renderer.render_packet.extract_render_packet(renderer: simvx.graphics.renderer.forward.Renderer, tree: simvx.core.SceneTree, *, frame_index: int = 0, sub_viewports: Any = None, item_view: simvx.graphics.render2d.publish.PublishedItemView | None = None, item_camera: tuple = (1.0, 0.0, 0.0, 1.0, 0.0, 0.0)) simvx.graphics.renderer.render_packet.RenderPacket

Snapshot the renderer’s current per-frame state into an owned RenderPacket.

Call on the MAIN thread immediately after adapter.submit_scene(tree), before Renderer.begin_frame clears the lists for the next frame. Numpy arrays are copied (ownership transferred to the packet) so the producer can safely rebuild the live lists while this packet is consumed by the render thread.

Args: renderer: The forward renderer whose per-frame lists to snapshot. tree: The scene tree (read for _structure_version and SubViewport discovery when sub_viewports is supplied). frame_index: Monotonic producer frame counter stamped on the packet. sub_viewports: Optional :class:SubViewportManager. When supplied the packet captures ordered SubViewport SRU plans (so the render thread can record them without walking the tree). None (the default and the unit-test path) yields an empty subviewport_srus. item_view: Optional published, immutable 2D item-pipeline view to attach. The caller obtains it from :meth:ItemPublisher.publish at this same sync point (inside _frame_state_lock). None (the unit-test path) leaves RenderPacket.item_view None (the scene drew no 2D).

class simvx.graphics.renderer.render_packet.RenderPacketRing(capacity: int = 2)

Bounded double-buffered handoff between the main and render threads.

The ring IS the double-buffer: a fixed capacity (default 2) of packet slots with backpressure. submit blocks the producer (main thread) once it is one frame ahead, bounding latency to +1 frame. acquire blocks the consumer (render thread) until a packet is available. The render thread calls release after it has finished the GPU frame for a packet, freeing the slot so the producer may run ahead again.

Shutdown is cooperative: close wakes any blocked thread. After close, submit raises and acquire drains remaining packets then returns None so the consumer loop exits cleanly.

Initialization

property capacity: int
property closed: bool
submit(packet: simvx.graphics.renderer.render_packet.RenderPacket) None

Producer: enqueue a packet, blocking while the ring is full.

Raises RuntimeError if the ring has been closed.

acquire(timeout: float | None = None) simvx.graphics.renderer.render_packet.RenderPacket | None

Consumer: dequeue the next packet, blocking until one is available.

Returns None if the ring is closed and drained (consumer should exit), or if timeout elapses with no packet.

release() None

Consumer: signal that the most recently acquired packet’s GPU frame is done.

Frees one in-flight slot so the producer may run ahead again.

close() None

Mark the ring closed and wake all blocked threads for clean shutdown.

pending() int

Number of packets queued but not yet acquired (diagnostics/tests).