simvx.graphics.render2d.publish¶
Render-thread publish/freeze + double-buffer for the item store.
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 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:
“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, so a future N-buffered GPU resource layers on top without changing the freeze.
Render-target-agnostic: 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. |
|
Double-buffered publish/freeze of the item store across the thread seam. |
Functions¶
Freeze a game-thread :class: |
Data¶
API¶
- simvx.graphics.render2d.publish.__all__¶
[‘ItemPublisher’, ‘PublishedItemView’, ‘freeze_collect_result’]
- class simvx.graphics.render2d.publish.PublishedItemView¶
An immutable, render-thread-readable snapshot of the item store.
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.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”. 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. Empty(0, 6)when there are no transforms. geometry The captured :class:Geometrytuples thegeometrycolumn resolves against (verts/indices), as tuples of Python lists. 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
- column(name: str) numpy.ndarray¶
Return the frozen (read-only) column
name.
- property nbytes: int¶
Total bytes of the frozen SoA columns + order + transforms.
Used by the concurrency probe to assert a clean frame is zero-extra-byte (the same view object is reused, so no new allocation happens at all).
- simvx.graphics.render2d.publish.freeze_collect_result(result: simvx.graphics.render2d.item_builder.CollectResult, *, version: int) simvx.graphics.render2d.publish.PublishedItemView¶
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¶
Double-buffered publish/freeze of the item store across the thread seam.
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 (a clean frame uploads nothing; the version stamp lets the render side reuse its buffers).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. 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: a single fence-gated SSBO is already race-free, so GPU resources that need more follow the existing
reserve_main_slicediscipline.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¶
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. Omitepochto fall back to pure identity reuse (frame-level skipping only).
- property published: simvx.graphics.render2d.publish.PublishedItemView | None¶
The currently published view (
Nonebefore the first :meth:publish).
- property version: int¶
The current publish version (incremented only on a dirty publish).
- property publish_count: int¶
How many frames froze a new view (dirty publishes). Test/perf introspection.
- property reuse_count: int¶
How many frames reused the existing view (clean, zero-byte). Introspection.