simvx.graphics.render2d.cache¶
The auto-invalidation contract (design §2.7 “what marks dirty”), the can’t-forget- a-property property¶
Nothing here hand-enumerates which Property dirties what. The cache reads the two
render-retention bits the core :class:~simvx.core._drawable2d.Drawable2D mixin
carries on every 2D drawable. Those bits are set by the EXISTING generic hooks:
the blanket
Property.__set__ -> obj.queue_redraw()hook (descriptors.py) on ANY changed Property of a drawable ->_render_dirty(P0 gate 3’s “no-op for Node2D” caveat is fixed:Node2Dnow definesqueue_redrawvia the mixin, so the hook fires for colour/text/visible/ texture/size/anchor/margin/…);the
_ObservedVec._notify -> _invalidate_transformpath + the direct_invalidate_transform()on spatial replacement (_spatial_property.py) ->_transform_render_dirty(move, including in-place+=);rotation’son_change="_invalidate_transform"->_transform_render_dirty;z_index/z_as_relative’son_change="_invalidate_z_cache"->_render_dirtyon self + descendants (z is structural, design §2.2);structural changes ->
SceneTree._structure_versionbump (compared here);theme generation -> a global counter compared here (the theme bridge, design §2.7: a
set_themeforces one non-skipped frame that re-collects themed Controls – they have no per-widget theme Property to fire the blanket hook);CanvasLayer.offset/rotation/scale_val/layer->on_changemarks the layer subtree render-dirty (CanvasLayer is aNode, covered by the mixin).
So a property write on a node marks the right dirty bit with no per-property list:
adding a new Property cannot “forget” to invalidate, because the descriptor hook
is blanket. The render bits are SEPARATE from Node2D._transform_dirty (which a
mid-frame world_position read clears) and persist until this cache drains them
at the end of the patch step (design §2.7 / P0 gate 3b).
The dynamic opt-out (design §2.7 “time-driven draws”, P0 gate 3c)¶
A node whose on_draw reads non-Property state (tree.now animations, an
AnimatedSprite frame advance) writes no tracked Property, so the hooks never
fire. Such a node is marked dynamic (a _render_dynamic attribute or the
cache-level predicate) and is treated as render-dirty EVERY frame; a subtree
containing a dynamic node never frame-skips. queue_redraw() remains the manual
escape hatch.
Render-thread safety (design §4)¶
The cache runs on the game thread and mutates the working store in place (item-
level / transform-only patches). The render thread reads only the published
frozen view (P1.5 publish/freeze), never the live store; an in-place patch is
visible to the render thread only after the next publish at the frame sync point.
The cache exposes :attr:epoch – bumped on every collect OR patch – so the
publisher freezes a new view after a patch (an in-place patch keeps the
CollectResult object identity, so identity-based reuse alone would wrongly
reuse the stale frozen view; the epoch is the change signal).
Retained render-item cache: frame + item + transform-only granularity (design §2.7).
The unified RenderItemCache (design §2.7 / Decision C / §7.6). It retains the
last collected+sorted item set for a scene and, riding the automatic
invalidation contract (P2), re-uploads only what changed:
frame level (P1.4) – whole-tree clean (no dirty drawable, structure + view + theme unchanged, not
dynamic) -> reuse the retained set verbatim, skipping walk + sort entirely;item level (P2) – a render-dirty drawable (a colour / texture / text / visible Property change, via the blanket descriptor hook now that
Node2Dhasqueue_redraw) re-captures only that node’son_drawand overwrites its geometry + item columns in place – one item re-uploaded, the rest retained;transform-only level (P2) – a transform-dirty drawable (a position / scale / rotation change, via the observed-vec /
_invalidate_transformpath) rewrites that node’s LOCAL transform row in place (the scroll-by-translation / moving-sprite fast path).
Module Contents¶
Classes¶
The view/UBO inputs a collection was built under (design §2.3 seam). |
|
Retains a scene’s collected items, re-uploading only what changed (design §2.7). |
Data¶
API¶
- simvx.graphics.render2d.cache.__all__¶
[‘RenderItemCache’, ‘ViewState’]
- class simvx.graphics.render2d.cache.ViewState(*, offset: tuple[float, float] = (0.0, 0.0), zoom: tuple[float, float] = (1.0, 1.0), rotation: float = 0.0, viewport: tuple[int, int] = (0, 0))[source]¶
The view/UBO inputs a collection was built under (design §2.3 seam).
A compact, equality-comparable bundle of the camera + viewport state that the frame-level skip compares frame to frame. Today (pre-P3b) any change forces a re-collect, because the per-frame view UBO that would let the view change without dirtying items is P3b work. The class exists so that seam is named: P3b relaxes “view changed -> re-collect” to “view changed -> rewrite one UBO row, items stay clean”.
Noneis a valid value (no camera / unknown view) and compares equal to anotherNoneview, so a scene with no Camera2D still skips when clean.Initialization
- __slots__¶
(‘offset’, ‘zoom’, ‘rotation’, ‘viewport’)
- class simvx.graphics.render2d.cache.RenderItemCache(*, builder_factory: collections.abc.Callable[[], simvx.graphics.render2d.item_builder.ItemBuilder] = ItemBuilder, dynamic: bool | collections.abc.Callable[[Any], bool] = False, theme_generation: collections.abc.Callable[[], int] | None = None)[source]¶
Retains a scene’s collected items, re-uploading only what changed (design §2.7).
One cache instance serves one scene root. Call :meth:
frameonce per render frame with the current structure-version and view state; it returns the- Class:
CollectResultto submit and applies the minimal update:
clean (nothing dirty, structure/view/theme unchanged, not dynamic) -> reuse the retained result verbatim, no walk, no patch (:attr:
epochunchanged);patchable (structure/view/theme unchanged, only some drawables dirty) -> patch those drawables in place: a transform-dirty node rewrites its LOCAL transform row; a render-dirty node re-captures its
on_drawand overwrites its geometry + item columns – the rest of the retained result untouched (:attr:epochbumped);full re-collect (first frame, structure-version / view / theme change, a dirty node not in the retained index, or
dynamic) -> re-walk + re-sortstore (:attr:
epochbumped).
Render-thread-safe with the P1.5 publish/freeze: the cache mutates the working store on the game thread; the render thread reads only the published frozen view. :attr:
epochlets the publisher freeze a fresh view after an in-place patch (which keeps theCollectResultobject identity).Initialization
Create a cache.
builder_factoryconstructs the collector run on a full re-collect (default: a fresh :class:ItemBuilderper collection). A test spy can pass a counting factory to prove the walk is skipped/patched, not re-run.dynamicis the scene-level opt-out (design §2.7 “time-driven draws”):True(or a predicate returning truthy for the root) makes the cache re-collect every frame. The defaultFalseadditionally honours per-nodedynamicmarking found during the dirty scan.theme_generationis the theme bridge (design §2.7): a zero-arg callable returning the global theme generation counter (ui.theme. theme_generation). When it changes, the cache forces one non-skipped re-collect so themed Controls (which carry no per-widget theme Property to fire the blanket hook) repaint under frame-skip.Nonedisables the bridge (a scene with no themed Controls).- __slots__¶
(‘_builder_factory’, ‘_dynamic’, ‘_cached’, ‘_cached_structure_version’, ‘_cached_view’, ‘_cached_th…
- mark_dirty() None[source]¶
Force a full re-collect on the next :meth:
frame(manual escape hatch).The automatic path (the descriptor / observed-vec / on_change hooks setting the per-drawable render bits) is the normal mechanism; this is the coarse override a test or a non-Property change can use.
- property dirty: bool[source]¶
Whether an explicit :meth:
mark_dirtyis pending (debug/introspection).
- is_dynamic(root: Any) bool[source]¶
Whether
root’s scene is time-driven at the SCENE level (design §2.7).Per-node
dynamicmarking (a_render_dynamicattribute on a node) is handled separately during the dirty scan; this is the whole-scene flag/ predicate the caller configured.
- set_dynamic(dynamic: bool | collections.abc.Callable[[Any], bool]) None[source]¶
Reconfigure the scene-level dynamic opt-out (flag or per-root predicate).
- frame(root: Any, *, structure_version: int = 0, view: simvx.graphics.render2d.cache.ViewState | None = None, layer: int = 0) simvx.graphics.render2d.item_builder.CollectResult[source]¶
Return the item set to submit this frame, re-uploading only what changed.
Decides between skip / in-place patch / full re-collect (see class doc), then drains the drawables’ render bits it consumed so the next frame starts clean. The returned :class:
CollectResultis the retained object (patched in place when patchable); read :attr:epochto tell whether it changed.
- property cached: simvx.graphics.render2d.item_builder.CollectResult | None[source]¶
The currently retained result (
Nonebefore the first frame).
- property epoch: int[source]¶
Monotonic change counter: bumped on every collect OR in-place patch.
The publish layer freezes a new view when the epoch advances; a clean (skipped) frame leaves it unchanged, so the publisher reuses the frozen view verbatim (the §4 zero-byte clean frame).
- property collect_count: int[source]¶
How many frames ran a full re-collect (the walk). Test/perf introspection.
- property patch_count: int[source]¶
How many frames applied an in-place item/transform patch (no full walk).
- property skip_count: int[source]¶
How many frames skipped entirely (clean: reused the retained set).
- property last_skipped: bool[source]¶
Whether the most recent :meth:
frameskipped (clean, no upload).