simvx.core.graphics.texture

Texture – an image the engine owns, with an identity, a version and an update path.

Module Contents

Classes

Texture

An image resource: an identity, a version, and an explicit way to change it.

Functions

is_live_target

Whether source is a node that publishes a live offscreen image.

Data

API

simvx.core.graphics.texture.__all__

[‘Texture’, ‘is_live_target’]

simvx.core.graphics.texture.is_live_target(source: Any) bool[source]

Whether source is a node that publishes a live offscreen image.

The one question every consumer that ROUTES a live feed asks: a sprite resolving its bindless slot each draw, a material binding an albedo, a backend skipping the lazy path/bytes load. Both kinds answer it – a

Class:

~simvx.core.SubViewport, which renders its own subtree, and a

Class:

~simvx.core.RenderView, which renders the main scene through a second camera – so no consumer has to name one kind and mean both.

class simvx.core.graphics.texture.Texture(source: Any, *, filter: str | None = None, colour_space: str | None = None, premultiply_alpha: bool | None = None, mipmaps: bool | None = None)[source]

An image resource: an identity, a version, and an explicit way to change it.

A raw texture source (a path, encoded bytes, an RGBA ndarray) answers neither “is this the same image as before” nor “has it changed”. A Texture answers both: its identity is stable across reassignment, and

Meth:

update bumps :attr:version, which is what drives a re-upload.

Wrapping is optional. Every node property that takes a texture still accepts a plain path, bytes or ndarray, and always will – that is the short spelling and it is not going away. Reach for Texture when the image CHANGES after it is first shown::

heightmap = Texture(pixels)          # RGBA uint8 (H, W, 4)
sprite.texture = heightmap
...
pixels[:] = erode(pixels)
heightmap.update()                   # re-uploads in place, same slot

icon = Texture("ui/icon.png", filter="nearest")
sprite.texture = icon
Meth:

update may also swap the pixels outright (heightmap.update(new_pixels)), which is the cheaper route when the replacement is a fresh array rather than an in-place edit. What it cannot do is change the SIZE: that is fixed when the texture is constructed, and a differently sized image is a different texture. Build one and assign it over the old, which re-resolves every consumer.

The sampling settings are fixed at construction: they select which backend slot the image lands in, so changing one is a different texture, not an update to this one. Each one you leave out stays unset, and an unset setting defers to whatever the consumer asks for – the sprite’s filter, the material map role’s colour space and mip chain. Only a setting you state here overrides that, and it overrides it everywhere the texture is used::

sprite.texture = Texture(pixels)                     # follows sprite.filter
sprite.texture = Texture(pixels, filter="nearest")   # crisp, wherever it is drawn

One resource owns one backend slot, so it carries one sampling. Two uses that need different ones cannot both be served: the first wins and the second is reported. Give each its own Texture over the same source.

A scene file can carry a texture whose source is a FILE: it is written as the constructor call that rebuilds it, with exactly the sampling settings that were stated, and one held by several nodes is written once into a local so the reloaded scene shares it the same way. Pixels in memory have no source form – a scene is code, not an image container – so a texture over an array or over encoded bytes stops the save and is named in the report. Build that one in on_ready() and assign it there, which is where code belongs anyway.

A texture owns its image on the backend outright. Two resources over the same file do not share it, nor does one share with the same file assigned as a plain path, so :meth:update can never change an image something else asked for; the price is one extra copy on the GPU when a file is used both ways. Ownership runs the other way too: the backend slot is handed back when the resource is collected, so building a texture per level or per entity does not exhaust the texture table.

Initialization

Wrap source as an owned texture.

Every sampling setting is optional, and omitting one is not the same as choosing its usual default: an omitted setting follows the consumer’s own request, so the same texture is sampled the way a plain path or array would have been in that position. State one only to override that everywhere the texture is used.

Args: source: File path, encoded image bytes, or an RGBA uint8 ndarray of shape (H, W, 4) – the same set every texture property accepts. A live offscreen target (a SubViewport, a RenderView) is refused: it is a texture source already, so assign the node itself where a live feed is sampled – either kind to sprite.texture or to an albedo_map. filter: Sampler filter, "linear" or "nearest". Unset follows the consumer (a sprite’s filter property). colour_space: "srgb" (perceptual colour) or "linear" (data maps: normals, metallic-roughness, AO). Unset follows the consumer, which is what keeps a texture used as a normal_map linear without being told twice. premultiply_alpha: Multiply RGB by alpha before upload. Unset follows the consumer. mipmaps: Generate a runtime mip chain. Unset follows the consumer, which is what keeps an albedo_map’s chain.

__slots__

(‘_uid’, ‘_source’, ‘_version’, ‘_size’, ‘_filter’, ‘_colour_space’, ‘_premultiply_alpha’, ‘_mipmaps…

property source: Any[source]

The underlying path / bytes / ndarray.

property version: int[source]

Increments on every :meth:update. Backends re-upload when it moves.

property filter: str | None[source]

Sampler filter, or None when it follows the consumer’s request.

property colour_space: str | None[source]

Colour space, or None when it follows the consumer’s request.

property premultiply_alpha: bool | None[source]

Premultiplication, or None when it follows the consumer’s request.

property mipmaps: bool | None[source]

Mip-chain request, or None when it follows the consumer’s request.

property size: tuple[int, int][source]

Native pixel (width, height), or (0, 0) when it cannot be read.

Once known, the size never changes: :meth:update refuses pixels of a different size, so anything that measures a texture once – a sprite drawing at native size, a nine-patch normalising its border UVs – stays correct without watching for changes.

Resolved without the GPU (an ndarray’s shape, or the image’s header), so it answers identically on every backend and before the first upload. Only a source no header reader here can measure answers (0, 0), and only until a backend decodes it and reports the dimensions back – which on a runtime with no imaging library means the first upload.

update(pixels: Any = None) None[source]

Replace this texture’s pixels, keeping its size, its slot and its identity.

This is the change-the-image path and only that. Call it after mutating the pixel array in place, or pass pixels to swap the array outright::

pixels[:] = erode(pixels)
heightmap.update()                  # or: heightmap.update(new_pixels)

Either way :attr:version moves and every backend holding a slot for this resource writes the new pixels into that SAME slot, so anything already drawing the texture keeps its handle and shows the new image on the next frame. Only consumers of this resource are affected: the slot is the resource’s own, never one shared with a plain path or array.

Resizing is a different operation, and this is not it. Consumers measure a texture when they resolve it – a sprite sizes its quad to the native pixels, a :class:~simvx.core.NinePatchRect normalises every border UV by them – so a slot that changed shape underneath them would draw the new image at the old scale, or sample the wrong region, with nothing to say so. A differently sized image is a different texture::

sprite.texture = Texture(bigger_pixels)

Reassignment re-resolves every consumer, which is what makes it the right answer rather than a workaround.

Args: pixels: Replacement source of the same size and pixel format, or None to re-upload the current one after an in-place edit.

Raises: ValueError: If the pixels differ in size or format from this texture’s. Nothing is mutated and no backend is told, so a refused update leaves every slot exactly as it was. TypeError: If pixels is a live offscreen target, refused here for the same reason the constructor refuses one – and equally without mutating anything. A live target measures as a real size, so a size check cannot be trusted to catch one: when the sizes match it passes straight through, and when they do not it is reported as a resize.

__copy__() simvx.core.graphics.texture.Texture[source]
__deepcopy__(memo: dict) simvx.core.graphics.texture.Texture[source]
__getstate__() tuple[dict | None, dict][source]

Pickle everything but the process-local state.

_managers holds weak references to live backend managers, which no pickle can carry; _uid is only meaningful in the process that minted it. Both are rebuilt by :meth:__setstate__.

__setstate__(state: Any) None[source]

Restore a pickled texture, minting a fresh identity.

The restored texture takes a new _uid rather than a number that may already belong to a live texture here, and an empty _managers: no backend holds a slot for it until something resolves it.

__repr__() str[source]