simvx.graphics.renderer.buffer_manager¶
BufferManager: owns the forward renderer’s SSBOs and descriptor sets.
Extracted from Renderer so transform/material/light/shadow/joint
buffers and the Forward+ tile bindings live in one place. The
descriptor set layout and cubemap placeholder are owned here too so the
renderer can swap the IBL cubemap in via write_cubemap_descriptor.
Bindings 5 and 6 start on 16-byte placeholders and are repointed at the light
cull pass’s per-frame tile buffers by write_tile_buffers the first time a
frame arms tiled lighting. The placeholders are never freed, so no binding index
moves and a scene that never asks for the feature keeps the layout it always had.
Module Contents¶
Classes¶
Owns the renderer’s SSBOs and descriptor sets. |
Data¶
API¶
- simvx.graphics.renderer.buffer_manager.__all__¶
[‘BufferManager’, ‘SHADOW_DATA_SIZE’]
- simvx.graphics.renderer.buffer_manager.log¶
‘getLogger(…)’
- simvx.graphics.renderer.buffer_manager.MAX_PROBES¶
8
- simvx.graphics.renderer.buffer_manager.PROBE_BUFFER_SIZE¶
None
- simvx.graphics.renderer.buffer_manager.IRRADIANCE_VOLUME_HEADER_SIZE¶
64
- simvx.graphics.renderer.buffer_manager.IRRADIANCE_VOLUME_MAX_PROBES¶
256
- simvx.graphics.renderer.buffer_manager.IRRADIANCE_VOLUME_BUFFER_SIZE¶
None
- class simvx.graphics.renderer.buffer_manager.BufferManager(engine: Any, max_objects: int, max_materials: int = 1024, max_lights: int = 256, max_joints: int = 256)¶
Owns the renderer’s SSBOs and descriptor sets.
Main descriptor set (
ssbo_set) exposes eighteen bindings: 0: transforms, 1: materials, 2: lights, 3: shadow, 4: IBL cubemap sampler, 5: tile light indices, 6: tile info, 7/8/9: IBL irradiance cube / prefilter cube / BRDF 2D LUT, 10/11: reflection-probe irradiance / prefilter cube arrays, 12: reflection-probe box SSBO, 13: FrameGlobals UBO, 14: scene colour copy, 15: scene depth copy (written by the renderer’s :class:SceneCopyTargets), 16/17: indirect specular / diffuse hook textures (1x1 black until an SSR / SSGI producer writes them).Joint descriptor set (
joint_set) is set 2 binding 0 for skinned meshes.Initialization
- property transform_buf: Any¶
- property transform_mem: Any¶
- property aabb_buf: Any¶
- property aabb_mem: Any¶
- property material_buf: Any¶
- property material_mem: Any¶
- property light_buf: Any¶
- property light_mem: Any¶
- property light_bufs: list[Any]¶
Every ring slot’s light SSBO, in slot order.
For a pass that owns its own per-frame descriptor sets and must bind slot i’s buffer into slot i’s set. Binding one slot’s buffer into all of them is the failure this exists to make unwritable: that pass would then read a different frame’s lights than the forward pass shades with, on every frame the ring is not on that slot.
- property shadow_buf: Any¶
- property shadow_mem: Any¶
- property frame_globals_buf: Any¶
- property frame_globals_mem: Any¶
- property ssbo_set: Any¶
The current frame’s main SSBO descriptor set (what every pass binds).
- property joint_buf: Any¶
- property joint_mem: Any¶
- property joint_set: Any¶
The current frame’s joint descriptor set (bound for skinned meshes).
- setup() None¶
Allocate all SSBOs and descriptor sets.
- property transform_capacity: int¶
Total transform-SSBO slots available this frame (==
max_objects).
- begin_frame_arena() None¶
Reset the per-frame bump cursor. Call once at the start of every frame.
- property scene_render_units: int¶
Scene-render units that have uploaded transforms so far this frame.
One per unit, main scene included, so a frame that recorded no SubViewport, RenderView or probe face answers 1 once the main scene has uploaded. A pass whose per-frame state is one value for the whole command buffer (the Forward+ tile grid width) reads this to tell a single-unit frame from a multi-unit one.
- property last_frame_scene_render_units: int¶
What :attr:
scene_render_unitsreached by the end of the previous frame.Most offscreen units upload before the main scene records, so the running count already sees them. The editor’s play-mode game viewport does not: it hangs off
Engine.pre_render_callbackand records after the renderer’s ownpre_renderhas returned. A gate that must know whether the frame is single-unit reads this as well, and a viewport that has been on screen for one frame is then visible to it.
- reserve_slots(count: int) int¶
Reserve a contiguous range of
counttransform slots; return its base.Bump-allocates from the shared SSBO arena. On overflow (the running cursor would pass capacity) the request is clamped to what fits, a WARNING is logged once, and a grow to
next_pow2(needed)is scheduled for the next frame boundary. The high-water mark is updated either way so a proactive grow can fire before an actual overflow recurs.Returns the base slot. The caller records draws with
first_instance = base + local_indexand writes its transforms into the SSBO at byte offsetbase * TRANSFORM_DTYPE.itemsize.
- request_joint_capacity(joint_count: int) int¶
Reserve room for
joint_countbone matrices in the joint SSBO.Returns the number that fit in the CURRENT buffer (so the caller clamps its upload and never writes past the allocation). When the request exceeds capacity a grow is scheduled for the next frame boundary (mirrors the transform arena), so the dropped skeletons appear one frame later instead of corrupting device memory. Symmetric with the web
_ensureBoneBuf.
- maybe_grow() bool¶
Grow the transform/AABB, joint and material SSBOs if scheduled. Frame-boundary only.
Reallocates when a grow is pending (an overflow clamped this frame) OR the high-water mark has crossed
_GROW_THRESHOLDof capacity (proactive). Must run OUTSIDE command recording (begin_frame or post-submit) so the reallocated buffer + descriptor rewrite never race the GPU.Returns True when a grow changed the OBJECT capacity, which is what the renderer’s slot-aligned resources (indirect batches, visibility buffers) follow. The material SSBO has no slot-aligned dependents, so growing it alone reports no capacity change.
- write_probe_descriptors(irradiance_array_view: Any, prefilter_array_view: Any, sampler: Any) None¶
Bind the ReflectionProbePass’s cubemap arrays to the forward set (bindings 10/11).
- write_probe_buffer(data: numpy.ndarray) None¶
Upload the probe box SSBO bytes (count header + Probe array, binding 12).
- write_decals(data: numpy.ndarray) None¶
Upload the decal SSBO bytes (count header + Decal array + tile masks, b19).
datais the block fromdecal_pack.build_decal_buffer. The base (header + record) region is a fixed :data:DECAL_BUFFER_SIZE; when the screen-tile cull is active the per-tile bitmask array is appended, so the block can exceed the initial allocation. It is grown (and its b19 descriptor repointed) on demand; the growth only ever adds tile-mask capacity, so it never perturbs the record region. An all-zero block (no decals) leavesdecal_countat 0, a byte-identical no-op.
- write_irradiance_volume_header(header: numpy.ndarray) None¶
Upload ONLY the volume SSBO header (64 B: grid + bounds + params, binding 18).
The per-probe SH region that follows is written by the IrradianceVolumePass SH-reduce compute (GPU), so the header upload must never touch it.
- write_ibl_descriptors(irradiance_view: Any, prefilter_view: Any, brdf_view: Any, sampler: Any) None¶
Bind an IBLPass’s precomputed maps to the forward set (bindings 7/8/9).
- upload_transforms(instances: list, *, upload_aabbs: bool = False, base: int = 0, pack_mesh_ids: bool = False) numpy.ndarray | None¶
Upload instance transforms + normal matrices + material ids into the SSBO slice.
Writes into the shared transform SSBO starting at slot
base(the slice reserved for this scene-render unit;base == 0for the main scene). Returns the (N, 4, 4) column-major (GLSL-ready) model matrices so the caller can feed the exact same data to the TAA velocity pass (prev-frame transform plumbing). Returns None when there are no instances.When
upload_aabbsis True (GPU occlusion culling enabled) the per-slot LOCAL AABB SSBO is populated alongside. Off by default so the occlusion-off path performs no extra host upload (byte-identical default behaviour).When
pack_mesh_idsis True, :attr:instance_blockis left holding what was uploaded – the same model matrices, plus the mesh and material index per slot – for consumers that need to know whether this frame’s block differs from the last one’s; it is None otherwise. Off by default, likeupload_aabbs, because only the positional shadow row cache reads it and gathering the mesh ids is a second pass over the instance list: packing them unconditionally cost the pack loop about a tenth of itself on frames with no positional shadow caster to spend it on.Exactly one call per scene-render unit that draws, whether or not it has instances, which is what :attr:
scene_render_unitscounts. MultiMesh blocks go through :meth:upload_transform_blockinstead, so a scene full of them is still one unit.
- upload_transform_block(transforms: numpy.ndarray, base: int, *, material_index: int = 0, material_ids: numpy.ndarray | None = None) numpy.ndarray | None¶
Vectorized upload of a contiguous instance block (e.g. a MultiMesh).
Unlike :meth:
upload_transforms, which iterates a Python list of per- instance tuples, this packs the whole(N, 4, 4)block with no Python loop and writes it at slots[base, base+N). Returns the world-space instance centres(N, 3)(translation columns) so the caller can frustum-cull the block without re-reading the SSBO. Returns None when the block does not fit.
- set_materials(materials: numpy.ndarray) numpy.ndarray¶
Upload the material array, growing the SSBO when a frame outgrows it.
Returns the (possibly clamped) array stored. A frame needing more rows than the buffer holds renders with the rows that fit and schedules a grow for the next frame boundary (:meth:
maybe_grow), exactly as the transform arena and the joint buffer do: reallocating here would pull the buffer out from under the frames still in flight. The high-water mark is tracked either way, so the proactive grow usually fires before an overflow.The WARNING is once per overflow episode, not once per frame: an episode ends when the buffer grows or when a frame fits again. A manager driven without frame boundaries (nothing calls :meth:
maybe_grow) therefore still reports every episode rather than falling silent after the first.The upload targets the recording frame’s ring slot and is not gated on the content having changed. Gating it cost a
hash(...tobytes()), which serialises the whole array to decide whether to copy it, and the array is a few tens of kilobytes.
- set_lights(lights: numpy.ndarray) None¶
Upload the light array behind its count header (GLSL
LightBufferlayout).Clamped to :attr:
max_lights, with the header carrying the clamped count so no shader reads past what was written. Nothing upstream limits how many lights a scene submits, and the block is- Data:
_LIGHT_HEADER_BYTESlonger than the array it carries, so an unclamped upload of a scene at capacity runs off the end of the allocation.
Writes the recording frame’s ring slot, ungated. A scene may call this several times in one frame (the caster budget re-slots, an offscreen unit submits its own set), and every call lands in the same slot, which is the frame’s own; no call can reach the slot a submitted frame is reading.
- prime_frame_slots(materials: numpy.ndarray) None¶
Write materials, and an empty light block, into EVERY ring slot.
Setup only, and it is not the same job as the per-frame upload. A slot no frame has reached yet holds whatever the allocator left in it, and the light block’s first word is the count the fragment shader loops on, so an unseeded slot is not merely stale: it is a loop bound made of uninitialised memory.
The light block is seeded EMPTY rather than with a placeholder row. A count of zero is what the shader keys its “this scene has no lights” fallback on, and a zeroed row is not a harmless stand-in for it: the row reads as a directional light whose direction is the zero vector, which normalises to NaN.
- write_shadow_data(shadow_data: numpy.ndarray) None¶
Upload raw shadow SSBO bytes (used by shadow passes + IBL-only fallback).
- write_shadow_uint(field: str, value: int) None¶
Patch one
uintof the current frame’s shadow block, leaving the rest.Three passes write that block and each of them rewrites it whole, so a word the renderer owns per frame rather than any one of them (the Forward+ tile grid width) is published here, after they have run, instead of being threaded through all three. Raises on an unknown or wrong-typed field name, as the block’s other writers do.
- write_tile_buffers(index_bufs: list[Any], index_size: int, tile_bufs: list[Any], tile_size: int) None¶
Point bindings 5 and 6 at the light-cull pass’s ringed tile buffers.
Set i takes ring copy i: the fragment stage of the frame recording into set i must read the lists the compute of that same frame wrote, not the ones the other frame in flight is writing. This replaces the 16-byte placeholders bound at setup, which stay allocated for the device’s life so no binding index ever moves.
Neither binding carries
UPDATE_AFTER_BIND(seegpu/descriptors.py: only the combined image samplers do), so the caller must have waited the device out first, exactly as- Meth:
write_decalsdoes when it grows binding 19. Both callers do: the pass is built once, and a resize runs behind the waitrecreate_swapchainalready takes.
- write_cubemap_descriptor(view: Any, sampler: Any) None¶
Bind a cubemap view+sampler to the IBL slot (binding 4).
- update_frame_globals(now: float, env: simvx.graphics.frame_globals.FrameGlobalsEnv, internal_extent: tuple[int, int], output_extent: tuple[int, int], render_scale: float = 1.0) None¶
Repack + upload the per-frame FrameGlobals UBO (binding 13).
deltais derived from the scene clock (nowminus the previous call’snow) andframe_indexfrom an internal counter, so the block needs no renderer-wide frame clock.internal_extentis the HDR-chain render extent,output_extentthe swapchain extent; they are equal (andrender_scale1.0) on the unscaled path, packing the exact pre-scale block.
- cleanup() None¶
Destroy all buffers, descriptor pools/layouts, and placeholder cubemap.