simvx.graphics.render2d.publish¶
Render-thread publish/freeze + double-buffer for the item store (design §4).
Increment P1.5 of the build-once 2D pipeline. The retained item store
(:class:~simvx.graphics.render2d.cache.RenderItemCache over
- class:
~simvx.graphics.render2d.item_list.ItemList) is mutable and lives on the game thread. The pipelined render thread, however, snapshots the legacyDraw2D._opsinto an owned per-frame copy precisely because the game thread mutates it (render_packet.pydocstring: the op list “previously raced”). A naively game-thread-mutated retained store would reintroduce exactly that race.
This module supplies the design §4 contract that makes the retained store safe to hand to the render thread:
The game thread mutates the working :class:
ItemListcolumns (via the cache).At the frame sync point (inside
_frame_state_lockwhere- func:
extract_render_packetruns) the store is published: an immutable- class:
PublishedItemViewsnapshot the render thread reads, never the live game-thread columns.
A clean frame republishes the SAME view object (zero copy, zero bytes): the cache returned the identical retained :class:
CollectResult, so the publisher recognises it and hands back last frame’s frozen view unchanged.A dirty frame freezes a new immutable view from the fresh columns. This is the double-buffer: the previous view stays valid for any render thread still reading it while the next view is built.
Why this is correct (design §4):
“clean frame uploads nothing” is consistent with “render thread reads an owned copy” because the clean-frame snapshot is a version stamp + buffer reuse, not a re-copy (the published view is the same object, identity-equal).
The frozen columns are contiguous numpy arrays with
writeable=Falseset, so the render thread can never observe a torn / half-written column while the game thread rebuilds the working :class:ItemListfor the next frame. They are also GPU-uploadable as-is – forward-compatible with the P1.6 GPU submit and the later N-buffered GPU resources (design §4: “Keep the columns GPU-uploadable”).
Render-target-agnostic (design §2.6): a :class:PublishedItemView is just frozen
columns + the tables they reference; the same publish path serves the main
framebuffer and a SubViewport target (the SubViewport SRU snapshots the published
view for its target).
Module Contents¶
Classes¶
An immutable, render-thread-readable snapshot of the item store (design §4). |
|
Double-buffered publish/freeze of the item store across the thread seam (design §4). |
Functions¶
Freeze a game-thread :class: |
Data¶
API¶
- simvx.graphics.render2d.publish.__all__¶
[‘ItemPublisher’, ‘PublishedItemView’, ‘freeze_collect_result’]
- class simvx.graphics.render2d.publish.PublishedItemView[source]¶
An immutable, render-thread-readable snapshot of the item store (design §4).
Frozen at the frame sync point from a game-thread :class:
CollectResult. The render thread reads this (never the live, mutating columns). Every numpy array is contiguous and haswriteable=Falseset, so a torn / half-written read is impossible and the columns are GPU-uploadable as-is (forward- compatible with P1.6’s GPU buffers and N-buffered resources).The double-buffer is realised by the publisher handing back the same view object on a clean frame (identity-equal, zero-copy) and a new one on a dirty frame – so a render thread still reading the previous view is never disturbed while the next is built.
Attributes
version Monotonic publish version. A clean frame reuses the previous view (and therefore its version); a dirty frame increments it. The render thread can compare versions to decide “same buffers as last frame -> zero-byte upload” vs “re-upload changed columns” (the P1.6 hook). count Number of live items (the logical length of every column). columns Frozen SoA columns by name (
layer,z,seq,pipeline,clip_scope,blend,transform,geometry,texture,flags,item_id), each a read-only length-countarray matching the :data:item_list._COLUMNSdtypes. GPU-uploadable directly. order Physical row indices in back-to-front draw order (the :meth:ItemList.sorted_orderresult), frozen read-only. transforms Frozen(count_transforms, 6)float32 array of the per-item LOCAL affine rows thetransformcolumn indexes (design §2.3). Empty(0, 6)when there are no transforms. geometry The captured :class:Geometrytuples thegeometrycolumn resolves against (verts/indices). Tuples of Python lists today (the bridge geometry, P1.2); P1.6/P3a replace these with arena slices. They are frozen by convention (the publisher does not alias the live store’s mutable list – it takes its own tuple). clips The :class:ClipScopeTabletheclip_scopecolumn indexes. Frozen by convention (the render thread only reads scissor rects).- version: int¶
None
- count: int¶
None
- columns: dict[str, numpy.ndarray]¶
None
- order: numpy.ndarray¶
None
- transforms: numpy.ndarray¶
None
- geometry: tuple[simvx.graphics.render2d.item_builder.Geometry, ...]¶
None
- simvx.graphics.render2d.publish.freeze_collect_result(result: simvx.graphics.render2d.item_builder.CollectResult, *, version: int) simvx.graphics.render2d.publish.PublishedItemView[source]¶
Freeze a game-thread :class:
CollectResultinto a :class:PublishedItemView.Copies each live SoA column into an owned, contiguous, read-only array (the render thread must not observe the game thread rebuilding the working
- Class:
ItemList), materialises the draw order once, and packs the per-item LOCAL transform rows into a single(N, 6)float32 array (GPU-uploadable). Geometry tuples are taken into an owned tuple so the published view never aliases the live store’s mutable backing list.
The arrays are made read-only (
writeable=False) so an accidental render- thread write is caught rather than silently corrupting the next frame’s working store.
- class simvx.graphics.render2d.publish.ItemPublisher[source]¶
Double-buffered publish/freeze of the item store across the thread seam (design §4).
Lives on the game thread alongside one
- Class:
~simvx.graphics.render2d.cache.RenderItemCache. Each frame, after the cache has produced the frame’s :class:CollectResult, the publisher is asked to :meth:publishit at the sync point. It returns the immutable- Class:
PublishedItemViewthe render thread reads:
Clean frame – the cache returned the same retained
CollectResultobject (identity-equal to the last one published). The publisher recognises this and returns the same :class:PublishedItemViewunchanged: zero copy, zero new bytes, version unchanged (the §4 “clean frame uploads nothing” / “version stamp + buffer reuse” path).Dirty frame – a different
CollectResult(re-collected). The publisher freezes a new immutable view (incrementing the version) while the previous view stays valid for any render thread still reading it (the double-buffer).
The publisher does NOT lock: ordering is provided by the caller publishing inside
_frame_state_lockat theextract_render_packetsync point (design §4 P1 note). The publisher only owns the freeze + identity bookkeeping.A single fence-gated published view is race-free because the render thread is the sole reader and the game thread only ever replaces the published handle (never mutates a published view in place). N-buffering for multiple frames in flight is OPTIONAL and layers on top (design §4: a single fence-gated SSBO is already race-free; follow the existing
reserve_main_slicediscipline) – P1.6’s GPU resources add it if needed.Initialization
- __slots__¶
(‘_published’, ‘_source_result’, ‘_source_epoch’, ‘_version’, ‘_publish_count’, ‘_reuse_count’)
- publish(result: simvx.graphics.render2d.item_builder.CollectResult, *, epoch: int | None = None) simvx.graphics.render2d.publish.PublishedItemView[source]¶
Publish
resultas an immutable view; reuse the same view if unchanged.Call at the frame sync point (inside
_frame_state_lock). Returns the- Class:
PublishedItemViewthe render thread should read this frame.
Reuse requires BOTH the same
CollectResultobject AND (when supplied) the same cacheepoch: the- Class:
~simvx.graphics.render2d.cache.RenderItemCachereturns the exact same retained object on a clean (skipped) frame and on an in-place patched frame – but a patch advances the cache’sepoch, so passing it here re-freezes after a patch (the patched columns must reach the render thread) while a truly clean frame (same object, same epoch) still reuses the frozen view verbatim. Omitepoch(the P1.5 frame-level-only path) to fall back to pure identity reuse.
- property published: simvx.graphics.render2d.publish.PublishedItemView | None[source]¶
The currently published view (
Nonebefore the first :meth:publish).