Source code for simvx.core.graphics.texture_slot
"""The one seam a backend publishes a resolved texture handle through."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..node import Node
_Base = Node
else:
_Base = object
__all__ = ["TextureSlot"]
[docs]
class TextureSlot(_Base):
"""Mixin for a node that shows a texture and caches the backend's handle.
A backend resolves ``node.texture`` to a bindless slot and has to hand that
number back to the node. Doing it with a plain attribute write is the bug
this mixin removes: the write fires none of the invalidation a
:class:`~simvx.core.descriptors.Property` fires, so a node that drew nothing
while its slot was still ``-1`` keeps serving that empty draw out of the
retained 2D cache forever. It is silent -- an invisible node and no error --
and it was fixed once per backend, per node type, before it was fixed here.
So the slot is not assignable. :meth:`publish_texture_slot` is the only
route, it invalidates, and it is a no-op when the slot has not moved, which
is what lets an offscreen target republish its handle every frame without
dirtying anything.
The other half is re-resolution. ``_invalidate_texture_slot`` is the
``on_change`` hook on every ``texture`` property: reassigning the texture
drops the cached slot, which is what re-opens the backend's resolve gate.
Without it the first successful resolve latches and the node draws the old
image for the life of the scene.
"""
# The two fields below are this seam's own storage. Read them through
# ``texture_slot`` / ``_texture_id``; write them only through
# ``publish_texture_slot`` / ``publish_overlay_slot`` /
# ``_invalidate_texture_slot``. A direct write is the bug described above --
# it skips the redraw and the node keeps serving its old draw.
#: Backend handle for the image being drawn; ``-1`` until one is published.
_texture_slot: int = -1
#: Web only: the same image's id in the 2D overlay table, which is a
#: different id space from the 3D material table.
_web_overlay_slot: int = -1
@property
def _texture_id(self) -> int:
"""The published backend handle, or ``-1``. Read-only: see :meth:`publish_texture_slot`."""
return self._texture_slot
@_texture_id.setter
def _texture_id(self, slot: int) -> None:
raise AttributeError(
f"{type(self).__name__}._texture_id is published by the backend, not assigned. "
"Call publish_texture_slot(slot), which also invalidates the node's cached draw."
)
[docs]
@property
def texture_slot(self) -> int:
"""The published backend handle for the current texture, or ``-1``."""
return self._texture_slot
[docs]
def publish_texture_slot(self, slot: int) -> None:
"""Publish a resolved backend handle and invalidate the cached draw.
Idempotent: republishing the handle a node already has does nothing, so
a per-frame publish costs one comparison and never dirties the node.
"""
slot = int(slot)
if slot == self._texture_slot:
return
self._texture_slot = slot
self.queue_redraw()
[docs]
def publish_overlay_slot(self, slot: int) -> None:
"""Web: publish the 2D overlay-table handle and draw with it.
A combined 2D+3D export may have already published a 3D material-table
handle on this node; for a 2D overlay the overlay table is the right one,
so it wins.
"""
self._web_overlay_slot = int(slot)
self.publish_texture_slot(slot)
def _invalidate_texture_slot(self) -> None:
"""Drop the cached handles so the backend resolves the new texture."""
self._texture_slot = -1
self._web_overlay_slot = -1