simvx.graphics.renderer.shadow_cache

Which positional shadow rows still hold a usable map, and which must be redrawn.

A point or spot shadow map is a picture of geometry seen from a light. Almost every frame it is the same picture as last frame’s, and drawing it again is the most expensive thing the renderer does per caster: recording four point casters over 200 casting meshes measured around 15 ms of a 21 ms frame, against under a millisecond of GPU time in the same phase – the cost is six cube faces’ worth of draw commands per caster, not the drawing. So a row is only recorded again when something it depends on moved.

The dependencies are: the light that owns the row (its position, reach and, for a spot, its cone – everything the row’s projection is built from, plus which light it is at all), the geometry the pass would draw into it, and the material fields a cutout caster’s alpha test is computed from, down to the pixels behind the texture it samples. Nothing here is subscribed to: the light rows and the instance block are already flowing past per frame as flat arrays, and a comparison against last frame’s is a memcmp.

The bias is deliberate: this compares whole-scene geometry rather than per-row frusta, so a crate moving in one corner re-records every row. That is the conservative direction. A row that should have been redrawn and was not is a stale shadow, which nothing later can repair; a row redrawn for nothing costs only what the engine used to pay every frame anyway.

Module Contents

Classes

SceneGeometry

What the positional shadow pass would draw this frame, cheaply comparable.

ShadowRowCache

Remembers what each atlas row was recorded from, and answers what is stale.

Functions

caster_row_keys

Per atlas row, everything about its light the recorded map depends on.

albedo_pixel_state

Per material, how many times what its albedo slot resolves to was replaced.

shadow_material_state

data:

SHADOW_MATERIAL_COLUMNS, copied out of the live material table.

instance_geometry

Read this frame’s :class:SceneGeometry off the renderer.

Data

API

simvx.graphics.renderer.shadow_cache.__all__

[‘POINT_KEY_WIDTH’, ‘SHADOW_MATERIAL_COLUMNS’, ‘SPOT_KEY_WIDTH’, ‘SceneGeometry’, ‘ShadowRowCache’, …

simvx.graphics.renderer.shadow_cache.POINT_KEY_WIDTH

5

simvx.graphics.renderer.shadow_cache.SPOT_KEY_WIDTH

9

simvx.graphics.renderer.shadow_cache.SHADOW_MATERIAL_COLUMNS

(‘albedo’, ‘features’, ‘albedo_tex’, ‘alpha_mode’, ‘alpha_cutoff’, ‘uv_rotation’, ‘uv_offset’, ‘uv_s…

simvx.graphics.renderer.shadow_cache.caster_row_keys(lights: numpy.ndarray | None, rows: int) tuple[numpy.ndarray, numpy.ndarray]

Per atlas row, everything about its light the recorded map depends on.

Returns (point_keys, spot_keys), each (rows, width): row r holds the light index that owns that row followed by the light’s position, and its range (plus direction and outer cone for a spot). A row with no caster is all

Data:

_NO_OWNER, so a row changing hands – to another light, or to none – shows up as plainly as a light moving.

Slots come from params.w exactly as :meth:ShadowRenderer._casters reads them, which is what keeps the cache and the pass talking about the same rows.

simvx.graphics.renderer.shadow_cache.albedo_pixel_state(materials: numpy.ndarray, generations: dict[int, int] | None) numpy.ndarray

Per material, how many times what its albedo slot resolves to was replaced.

generations is the engine’s per-slot count of those replacements, which is the only thing that moves when Texture.update() writes new pixels into a bound slot, or a SubViewport renders another frame into its target: the material still names the same texture index.

Only alpha-tested materials carry a number, because they are the only ones a shadow pipeline samples a texture for – the opaque shadow shader reads no material at all, and the cutout pipeline is entered on exactly this test (_cutoff_caster_slots). So a video texture, or an IDE minimap, that no cutout caster reads re-records nothing.

simvx.graphics.renderer.shadow_cache.shadow_material_state(materials: numpy.ndarray | None, generations: dict[int, int] | None = None) numpy.ndarray | None
Data:

SHADOW_MATERIAL_COLUMNS, copied out of the live material table.

The copy is the point. The array the renderer holds is the material slot table’s own storage, and a Material that changes has its row rewritten in place: keeping a reference would mean comparing this frame’s table against itself, which never differs, and a caster that became alpha-tested would keep the solid shadow it had. Copying costs one small array a frame – the table is one row per registered material.

Func:

albedo_pixel_state adds the one thing the table itself cannot say: that the pixels behind a slot it names were replaced.

class simvx.graphics.renderer.shadow_cache.SceneGeometry

What the positional shadow pass would draw this frame, cheaply comparable.

Every member is either a flat array the frame already produced or a small tuple, so building one costs no traversal and comparing two costs a memcmp.

  • atlas_generation – the images the cached rows live in. A resize allocates new ones and everything cached with them is gone.

  • models – the per-instance model matrices uploaded to the transform SSBO. The recorded draws never name a transform (the vertex shader reads it by instance index), so this is the only thing that catches a mesh moving.

  • meshes / materials – the mesh and material index per instance slot, in slot order: a mesh swap or a material swap changes what is drawn, or whether it goes down the cutout path, without moving anything.

  • material_state – :func:shadow_material_state of the material table: what routes an instance through the cutout pipeline and what that pipeline’s discard is computed from, so a material becoming alpha-tested, an alpha test whose threshold or texture moved, or new pixels written into the texture it already had, changes the picture here too.

  • blocks – one entry per MultiMesh block: its identity, its content generation and the SSBO slots it landed on. None when a block carries no stable identity, which never compares equal: an anonymous block could have had its transforms rewritten under an unchanged shape.

  • instances_known – whether models / meshes / materials account for every instance the pass will draw. False on a frame that packed no block at all, because no light held a caster row and nothing was going to consult this, and on one whose instance list overflowed max_objects and was clamped on upload, which leaves the arrays describing less than the pass draws. A frame that does not know matches nothing, including another frame that does not know.

atlas_generation: int

None

models: numpy.ndarray | None

None

meshes: numpy.ndarray | None

None

materials: numpy.ndarray | None

None

material_state: numpy.ndarray | None

None

blocks: tuple[tuple[int, ...], ...] | None

()

instances_known: bool

True

matches(other: simvx.graphics.renderer.shadow_cache.SceneGeometry | None) bool

True when other would produce the same shadow maps as this frame.

class simvx.graphics.renderer.shadow_cache.ShadowRowCache

Remembers what each atlas row was recorded from, and answers what is stale.

One instance per renderer, asked once a frame. It holds a reference to last frame’s row keys and instance block rather than a copy: both are built fresh every frame and never written in place, so keeping them costs one frame’s worth of arrays and no copying. The material table is the one input that does outlive the frame and is edited in place, which is why

Func:

shadow_material_state copies what it takes from it.

Initialization

__slots__

(‘_geometry’, ‘_point_keys’, ‘_spot_keys’)

stale_rows(geometry: simvx.graphics.renderer.shadow_cache.SceneGeometry, point_keys: numpy.ndarray, spot_keys: numpy.ndarray) tuple[numpy.ndarray, numpy.ndarray]

Boolean masks over the atlas rows that must be recorded this frame.

Geometry is judged for the whole scene, so a change there stales every row of both kinds; the row keys then stale the individual rows whose light moved or changed hands. The frame’s state becomes the baseline the next call compares against, whatever the answer was.

simvx.graphics.renderer.shadow_cache.instance_geometry(renderer: Any, atlas_generation: int) simvx.graphics.renderer.shadow_cache.SceneGeometry

Read this frame’s :class:SceneGeometry off the renderer.

Called after the transform upload, which is where the flat per-instance arrays come from; the MultiMesh entries pair the blocks’ identity and content generation with the slots _upload_multimesh_blocks gave them.

A block that does not account for every instance the pass will draw – one the frame did not pack at all, or one the upload clamped at max_objects – is reported as such rather than compared, so an instance the arrays never mention cannot move behind the cache’s back.