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). The transform is
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.
draw2d_ops
Snapshot of Draw2D._ops (draw2d.py ~170): the ordered immediate-mode 2D
op list a HUD/overlay scene rebuilds every frame on the MAIN thread
(Draw2D._reset(); tree.render(Draw2D)). Op is an immutable
NamedTuple whose verts/indices lists Draw2D builds fresh per emit
and never mutates after appending, so a shallow list(...) copy is a
faithful owned snapshot. install_packet binds it as the op source the
render thread’s Draw2DPass reads, so the render thread never touches the
live global the main thread is concurrently clearing + rebuilding.
frame_index
Monotonic producer frame counter, for ordering / telemetry / debugging.
Subsystem submission buffers (packetised this wave)¶
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); the
tile_data structured array is 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.
text_vertices / text_indices
Owned copies of Renderer._text_renderer.vertices / .indices
(text_renderer.py ~316-326): the CPU MSDF geometry the shared TextRenderer
builds each frame from draw_text calls and that
OverlayRenderer.render_text uploads + draws via TextPass. The arrays
are copied because the TextRenderer reuses its vertex/index buffers next
frame (begin_frame resets _char_count and overwrites in place).
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 P1 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 isolated
Draw2D op list, 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_transformsrun there from the packet’sinstances). The main thread never reserves.Per-frame HDR / post flags (
_hdr_rendered): recomputed by the render thread insidepre_render/renderfrom 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 for this wave. Probe-bearing scenes keep the safe skip + one-time warning in pipelined mode (app.py_warn_pipelined_unpacketised/pre_render_fn).
Note: Draw2D._ops (immediate-mode 2D HUD/overlay) was previously in the
“not snapshotted” set and raced; it is now draw2d_ops (installed via
install_packet). 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
capture remains 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.
This is the FOUNDATION wave: the packet, its :func:extract_render_packet
builder, and the :class:RenderPacketRing double-buffer are defined and unit
tested, but no render thread is spawned and the synchronous frame loop is
unchanged. The default (synchronous) path never constructs a packet, so it stays
byte-identical.
Module Contents¶
Classes¶
Owned snapshot of one viewport’s camera + rect for a single frame. |
|
Owned plan to record one SubViewport offscreen without walking the tree. |
|
Owned snapshot of the renderer’s per-frame submission state. |
|
Bounded double-buffered handoff between the main and render threads. |
Functions¶
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[source]¶
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
Viewportnext frame (TAA jitter and the motion-blur matrix update both mutatecamera_projin 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[source]¶
Owned plan to record one SubViewport offscreen without walking the tree.
Captured on the MAIN thread by :meth:
SubViewportManager.build_srusfrom a SubViewport’s already-submitted offscreen scene. The render thread (next wave) replays it: reserve a transform-SSBO slice forinstances+skinned_instances, write those transforms, record the draws into the SubViewport’srenderertarget withcamera_view/camera_proj, then overlaydraw2d_ops. The producer-before-consumer ordering is encoded by this list’s position in :attr:RenderPacket.subviewport_srus(P1 topo 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. Mirrorsrender_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_bgdecides RGBA), copied. camera_view / camera_proj Owned copies of the SRU camera’s view + projection matrices (Nonefor 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). draw2d_ops Owned copy of the SubViewport subtree’s isolated Draw2D op list (the 2D overlay drawn on top of its 3D content viarender_draw2d).- 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
- draw2d_ops: list[Any]¶
‘field(…)’
- class simvx.graphics.renderer.render_packet.RenderPacket[source]¶
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]]¶
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(…)’
- draw2d_ops: list[Any]¶
‘field(…)’
- tilemap_layers: list[tuple[numpy.ndarray, int, tuple[float, float]]]¶
‘field(…)’
- light2d_lights: list[dict]¶
‘field(…)’
- light2d_occluders: list[list[tuple[float, float]]]¶
‘field(…)’
- text_vertices: numpy.ndarray | None¶
None
- text_indices: numpy.ndarray | None¶
None
- subviewport_srus: list[simvx.graphics.renderer.render_packet.SubViewportSRU]¶
‘field(…)’
- 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) simvx.graphics.renderer.render_packet.RenderPacket[source]¶
Snapshot the renderer’s current per-frame state into an owned RenderPacket.
Call on the MAIN thread immediately after
adapter.submit_scene(tree), beforeRenderer.begin_frameclears 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_versionand SubViewport discovery whensub_viewportsis 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 emptysubviewport_srus.
- class simvx.graphics.renderer.render_packet.RenderPacketRing(capacity: int = 2)[source]¶
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.submitblocks the producer (main thread) once it is one frame ahead, bounding latency to +1 frame (D6).acquireblocks the consumer (render thread) until a packet is available. The render thread callsreleaseafter it has finished the GPU frame for a packet, freeing the slot so the producer may run ahead again.Shutdown is cooperative:
closewakes any blocked thread. After close,submitraises andacquiredrains remaining packets then returnsNoneso the consumer loop exits cleanly.Initialization
- submit(packet: simvx.graphics.renderer.render_packet.RenderPacket) None[source]¶
Producer: enqueue a packet, blocking while the ring is full.
Raises
RuntimeErrorif the ring has been closed.
- acquire(timeout: float | None = None) simvx.graphics.renderer.render_packet.RenderPacket | None[source]¶
Consumer: dequeue the next packet, blocking until one is available.
Returns
Noneif the ring is closed and drained (consumer should exit), or iftimeoutelapses with no packet.