simvx.graphics.renderer.light_cull_pass

Forward+ tiled light culling via compute shader.

Dispatches a compute shader that assigns lights to 16x16 screen-space tiles using depth-aware frustum culling. The output buffers (light index list and per-tile offset/count) are bound to the fragment shader so it can loop over only the lights relevant to each pixel’s tile.

Every buffer here is per-frame by construction: the params block carries an atomic write head that the host resets on each dispatch, and the two outputs are written by compute and read by the next pass’s fragment stage. So all three are ringed to :data:FRAMES_IN_FLIGHT, along with the descriptor set that points at them, and a dispatch takes its slot from engine.current_frame. Without the ring, frame N’s host reset of the atomic would race frame N-1’s atomicAdd and frame N’s compute would overwrite the tile lists frame N-1 is still shading from.

This is a plain object rather than a RenderPass: the dispatch is recorded by an explicit call, the way the ocean and particle compute already are, and the setup signature takes an extent rather than a render context.

class:

~simvx.graphics.renderer.forward.Renderer owns it. It builds the pass on the first frame tiled_lighting_enabled is armed and the light count clears

data:

TILED_LIGHTING_MIN_LIGHTS, records the dispatch in pre_render before the shadow passes, and points forward-set bindings 5 and 6 at

attr:

~LightCullPass.light_index_buffers and :attr:~LightCullPass.tile_buffers.

Module Contents

Classes

LightCullPass

GPU tiled light culling for Forward+ rendering.

Data

API

simvx.graphics.renderer.light_cull_pass.__all__

[‘MAX_LIGHTS_PER_TILE’, ‘TILED_LIGHTING_MIN_LIGHTS’, ‘LightCullPass’]

simvx.graphics.renderer.light_cull_pass.log

‘getLogger(…)’

simvx.graphics.renderer.light_cull_pass.TILED_LIGHTING_MIN_LIGHTS

32

simvx.graphics.renderer.light_cull_pass.MAX_LIGHTS_PER_TILE

256

class simvx.graphics.renderer.light_cull_pass.LightCullPass(engine: Any)

GPU tiled light culling for Forward+ rendering.

Creates a compute pipeline that reads a depth texture and light SSBO, then outputs per-tile light index lists consumed by the fragment shader. The params block and both outputs are ringed one copy per frame in flight, with one descriptor set per copy; dispatch selects the slot.

Initialization

setup(width: int, height: int, max_lights: int = MAX_LIGHTS_PER_TILE) None

Create the compute pipeline, the ringed SSBOs and their descriptor sets.

Args: width: Viewport width in pixels. height: Viewport height in pixels. max_lights: Maximum number of lights in the scene. Retained, so a later resize rebuilds at the same capacity.

update_descriptors(depth_view: Any, light_ssbos: collections.abc.Sequence[Any], light_ssbo_size: int) None

Point every ring slot’s set at the depth texture (1) and its light SSBO (2).

The depth texture is one image every slot samples. The light SSBO is NOT: it is ringed across FRAMES_IN_FLIGHT, so slot i’s set must take slot i’s buffer, which is why this takes a sequence rather than a handle. Binding one buffer into every set would make the tile cull read a different frame’s lights than the forward pass shades with, on every frame the ring is not on that slot, and nothing downstream would say so.

The arguments are retained and replayed by setup, so a resize that rebuilds the sets does not silently drop them.

dispatch(cmd: Any, view_mat: numpy.ndarray, proj_mat: numpy.ndarray, light_count: int, near: float, far: float, depth_valid: bool = False) None

Record the light culling compute dispatch into a command buffer.

Must be called outside a render pass, after depth is available and before the main geometry pass that reads the tile data.

Args: cmd: Command buffer to record into. view_mat: Camera view matrix, row-major. proj_mat: Camera projection matrix, row-major. Inverted here and pushed inverted, which is the only form the shader uses. light_count: Number of lights in the light SSBO. near: Camera near plane distance. far: Camera far plane distance. depth_valid: True only when binding 1 carries this frame’s scene depth. While it is a placeholder view the shader must not reduce it, so the tile frusta span the full near-to-far slab and the cull is purely screen-space.

property ready: bool
property tile_buffers: list[Any]

Tile SSBO handles, one per frame in flight, in ring-slot order.

The forward set for slot i must be pointed at element i, or a frame shades from the tile lists another frame is writing.

property tile_buffer_size: int
property light_index_buffers: list[Any]

Light index SSBO handles, one per frame in flight, in ring-slot order.

property light_index_buffer_size: int
property max_lights: int

Per-tile light capacity this pass was set up with.

property grid_dims: tuple[int, int]
resize(width: int, height: int) None

Rebuild at a new extent, keeping the capacity setup was given.

A no-op when the tile grid is unchanged, so a resize within one tile of the same grid costs nothing. The caller must repoint whatever holds

Attr:

tile_buffers / :attr:light_index_buffers: the rebuild allocates new handles.

destroy() None

Destroy all GPU resources and drop every handle.

Every handle is cleared rather than left dangling, so a destroy followed by an inspection (or by a second destroy) cannot read freed memory.