Per-RenderList arena store for GPU-resident scene data. More...
Detailed Description
Per-RenderList arena store for GPU-resident scene data.
Owns one QRhiBuffer per well-known arena kind (camera UBO, light SSBO, material SSBO, per-draw SSBO, …) and hands out offset-based slots via a fixed-stride free-list. Source nodes (Camera, Light, PBRMesh, …) hold a slot for their lifetime and write their packed bytes into it at their own update(); the preprocessor binds the registry's buffers as scene auxiliaries. No CPU→GPU work happens in the preprocessor's render path — every upload is gated to a source-node message.
Also owns the per-channel material texture arrays (baseColorArray, metalRoughArray, …) and the mesh attribute slabs.
Lifetime: owned by the OutputNode, init()'d once per QRhi from RenderList::init and torn down with the QRhi. Not thread-safe — all calls must come from the render thread.
Classes | |
| struct | MeshSlab |
| Slab handle returned by MeshArenaManager::acquire. More... | |
| struct | Slot |
| struct | TextureChannelState |
| Channel texture state with multi-bucket support. More... | |
Public Member Functions | |
| GpuResourceRegistry (const GpuResourceRegistry &)=delete | |
| GpuResourceRegistry & | operator= (const GpuResourceRegistry &)=delete |
| void | init (QRhi &rhi, QRhiResourceUpdateBatch &batch) |
| Create the arena buffers. Must be called before any allocate(). | |
| bool | isInitialized () const noexcept |
| True if init() has been called and destroyOwned()/destroy() has not. Used by RenderList::init to gate the (otherwise asserting) init() call when the registry is being reused across an RL rebuild. | |
| QRhi * | boundRhi () const noexcept |
| QRhi this registry was init()'d against. Null when not initialised. The owning OutputNode uses this to decide whether the registry is still bound to its QRhi (vs. a fresh QRhi created after a setSwapchainFormat-style teardown). | |
| void | seedDefaults (QRhiResourceUpdateBatch &batch) |
| Seed reserved arena slots with sensible defaults. | |
| void | destroy (RenderList &renderer) |
| Destroy the arena buffers via the owning RenderList. | |
| void | destroy () |
| Destructor fallback — buffers are nulled without touching the QRhi. Only safe when destroy(RenderList&) has already run (or when the QRhi has already torn them down as children). Leaks the QRhiBuffer wrappers otherwise; that's the lesser evil vs. a use-after-free in the common "QRhi already dead" path. | |
| void | destroyOwned () |
| Tear down arena buffers + texture arrays + mesh streams directly (no RenderList plumbing). Called by the owning OutputNode when its QRhi is about to be destroyed (destroyOutput, ~OutputNode). | |
| Slot | allocate (Arena arena, uint32_t size) |
Reserve a slot in the given arena for size bytes. | |
| void | free (Slot &slot) |
| Return the slot to the free list. Safe to call with invalid Slot. | |
| QRhiBuffer * | buffer (Arena arena) const noexcept |
| Buffer underlying an arena. Null until init(). | |
| uint32_t | slotOffset (const Slot &slot) const noexcept |
| Byte offset of a slot inside its arena's buffer. | |
| uint32_t | arenaSlotStride (Arena arena) const noexcept |
Byte stride of the arena — every slot is this many bytes. Consumer shaders index arena.entries[slot_index] where entries[] has std430 stride equal to this value. | |
| uint32_t | arenaSlotCount (Arena arena) const noexcept |
| Slot capacity of the arena (number of slots, not bytes). | |
| void | updateSlot (QRhiResourceUpdateBatch &res, const Slot &slot, const void *data, uint32_t size) noexcept |
Upload size bytes starting at data into a slot. | |
| ossia::gpu_slot_ref | toOssiaRef (const Slot &slot) const noexcept |
| Produce an ossia::gpu_slot_ref that can be stamped on a scene-graph component for the downstream preprocessor to consume. | |
| bool | isLiveIn (const ossia::gpu_slot_ref &r, Arena expected) const noexcept |
| isLive, additionally requiring the ref to name the expected arena. | |
| bool | isLive (const ossia::gpu_slot_ref &r) const noexcept |
| Return true if the ref still points at a live allocation. | |
| TextureChannelState & | textureChannel (TextureChannel ch) noexcept |
| Shared state for one of the PBR texture channels. Preprocessors / producers read-modify this in place; contents are view-independent (asset identity drives layer assignment) so sharing across preprocessors is correct. | |
| const TextureChannelState & | textureChannel (TextureChannel ch) const noexcept |
| int | resolveDynamicSlot (TextureChannel channel, void *native_handle) noexcept |
| Register a runtime GPU texture handle for this channel's dynamic-slot set. Returns the slot index (0 .. kMaxDynamicSlots-1) or -1 if the slot cap is exhausted. | |
| MeshSlab * | acquireMeshSlab (uint64_t stable_id, uint32_t vertex_count, uint32_t index_count, uint32_t current_frame) noexcept |
| void | markMeshSlabSeen (uint64_t stable_id, uint32_t current_frame) noexcept |
| Mark a slab as seen this frame so sweep() doesn't reclaim it. | |
| void | sweepMeshSlabs (uint32_t current_frame, uint32_t grace=2) noexcept |
| void | sweepStaleDynamicTextureSlots () noexcept |
| void | drainExpiredPendingReleases (uint32_t current_frame, uint32_t grace=2) noexcept |
| void | releaseMeshSlab (uint64_t stable_id, uint32_t current_frame) noexcept |
| uint32_t | meshSlabOffsetBytes (const MeshSlab &slab, MeshStream stream) const noexcept |
| QRhiBuffer * | meshStreamBuffer (MeshStream s) const noexcept |
| void | uploadMeshStream (QRhiResourceUpdateBatch &res, const MeshSlab &slab, MeshStream s, const void *data, uint32_t size) noexcept |
| uint32_t | meshStreamUsedBytes (MeshStream s) const noexcept |
| Total bytes in use per stream (for the telemetry panel). | |
| uint32_t | meshStreamFreeBytes (MeshStream s) const noexcept |
Static Public Attributes | |
| static constexpr int | kTextureLayerSize = 1024 |
| static constexpr int | kMaxDynamicSlots = 4 |
| static constexpr int | kMaxBuckets = 16 |
| static constexpr uint32_t | kMeshStride [(std::size_t) MeshStream::Count_] = {16, 16, 8, 16, 16, 8, 4} |
| static constexpr uint32_t | kMeshCapBytes [(std::size_t) MeshStream::Count_] |
Member Function Documentation
◆ acquireMeshSlab()
|
noexcept |
Acquire a slab for a mesh: an existing slab on stable_id hit, a fresh allocation on miss, nullptr on allocator exhaustion. freshly_allocated on the returned slab means the caller must upload the bytes through uploadMeshStream().
current_frame stamps a real release frame on the count-mismatch grace-queue enqueue; without it every deferred release would be freed on the next sweep, defeating the guard against in-flight draws.
◆ allocate()
| GpuResourceRegistry::Slot score::gfx::GpuResourceRegistry::allocate | ( | Arena | arena, |
| uint32_t | size | ||
| ) |
Reserve a slot in the given arena for size bytes.
- Returns
- invalid Slot on OOM. Caller must check Slot::valid().
◆ buffer()
|
noexcept |
◆ destroy()
| void score::gfx::GpuResourceRegistry::destroy | ( | RenderList & | renderer | ) |
Destroy the arena buffers via the owning RenderList.
Every arena QRhiBuffer is routed through RenderList::releaseBuffer so the RenderList's bookkeeping sees the release and any other path that still holds a pointer to the buffer can't accidentally double- free it. Prefer this overload; call it from RenderList::release() before the QRhi teardown.
◆ destroyOwned()
| void score::gfx::GpuResourceRegistry::destroyOwned | ( | ) |
Tear down arena buffers + texture arrays + mesh streams directly (no RenderList plumbing). Called by the owning OutputNode when its QRhi is about to be destroyed (destroyOutput, ~OutputNode).
We delete the QRhiBuffer / QRhiTexture / QRhiSampler wrappers directly: the QRhi is still alive at this call site (callers MUST invoke this BEFORE RenderState::destroy() / setSwapchainFormat-style teardown), so the destructors free both the wrapper and the underlying GPU resource cleanly. After this call the registry is back to its pre-init() state and can be re-init()'d against a new QRhi.
◆ drainExpiredPendingReleases()
|
noexcept |
Free pending-release slabs whose released_frame + grace <= current_frame. Called by sweepMeshSlabs and by acquireMeshSlab before its fresh allocate, so a count-mismatch whose previous slot has served its grace recycles that capacity in the same update() instead of a spurious "pool exhausted". Leaves the *SlotsUsed trackers alone: those are decremented eagerly at enqueue time.
◆ init()
| void score::gfx::GpuResourceRegistry::init | ( | QRhi & | rhi, |
| QRhiResourceUpdateBatch & | batch | ||
| ) |
Create the arena buffers. Must be called before any allocate().
Per-arena capacity is fixed at init time (grow-in-place reallocation is a follow-up). If an arena runs out of room, allocate() returns an invalid Slot and logs a warning.
Persist-across-rebuild contract: the registry lives on the OutputNode and survives RenderList rebuilds (e.g. viewport resize). init() is called lazily, once per QRhi lifetime; later createRenderList calls reuse the registry as-is (texture arrays, mesh slabs, arena slot generations all preserved). isInitialized() tells the two paths apart.
◆ isLive()
|
inlinenoexcept |
Return true if the ref still points at a live allocation.
O(1): one array access + one uint32 compare. The generation table is bumped on every allocate() and free(), so a ref from a prior allocation at the same slot index fails the compare.
◆ isLiveIn()
|
inlinenoexcept |
isLive, additionally requiring the ref to name the expected arena.
isLive() validates internal_index against whichever arena the ref names, so a ref belonging to another arena still passes: a RawTransform slot (16384 of them) validates fine and yields an index up to 16383. raw_slot is the same ossia::gpu_slot_ref field on transform, light, camera and material components, so only producer discipline keeps them apart, and one crossed assignment has a consumer reading far outside what was sized for it.
Use this wherever a slot index is about to be handed to a shader as an index into a specific arena's mirror buffers.
◆ meshSlabOffsetBytes()
|
noexcept |
Byte offset of a stream within its backing buffer. Use directly as uploadStaticBuffer(buf, offset, size, data).
◆ meshStreamBuffer()
|
noexcept |
Backing QRhiBuffer for a stream. Stable pointer across the registry's lifetime (pre-sized, never grown).
◆ releaseMeshSlab()
|
noexcept |
Explicit release of a slab by stable_id (used on scene teardown). The release is enqueued into the pending-releases grace queue and freed from the OffsetAllocator only after grace frames have elapsed, matching the same contract as sweepMeshSlabs. Pass the current render-frame counter so the sweep can determine when it is safe to reclaim the sub-allocation.
◆ resolveDynamicSlot()
|
noexcept |
Register a runtime GPU texture handle for this channel's dynamic-slot set. Returns the slot index (0 .. kMaxDynamicSlots-1) or -1 if the slot cap is exhausted.
A handle keeps its slot for as long as it stays live: every resolve stamps the slot with a fresh access counter, so within one frame multiple producers and the preprocessor all get the same answer for the same handle. Slots freed by sweepStaleDynamicTextureSlots() are reused first; once the channel is at kMaxDynamicSlots the least recently used slot is evicted.
◆ seedDefaults()
| void score::gfx::GpuResourceRegistry::seedDefaults | ( | QRhiResourceUpdateBatch & | batch | ) |
Seed reserved arena slots with sensible defaults.
Called by the owning RenderList after init(), once the initial resource-update batch is ready. Writes a default white-dielectric MaterialGPU into Material arena slot 0 — the slot arenaSlotForMaterial(nullptr) returns for a draw with no material assigned. Without the seed that slot holds whatever bytes the last registered material left behind.
Idempotent — second call is a no-op once m_defaults_seeded is set.
◆ sweepMeshSlabs()
|
noexcept |
Release slabs whose last_seen_frame < current_frame - grace. Grace defaults to 2 (covers FramesInFlight+1 on typical backends).
◆ sweepStaleDynamicTextureSlots()
|
noexcept |
Clear dynamic texture slots not re-resolved by any live material since the previous sweep, so the consumer's "bind every non-null dynamic slot" loop can never bind a dangling pointer. Must run once per frame after the resolveDynamicSlot pass and before the slots are bound; the call site inside sweepMeshSlabs satisfies that, since ScenePreprocessor::update() runs rebuildChannel before rebuildMDI().
◆ toOssiaRef()
|
inlinenoexcept |
Produce an ossia::gpu_slot_ref that can be stamped on a scene-graph component for the downstream preprocessor to consume.
The returned ref captures (arena tag, offset, size, internal slot index, generation). The preprocessor uses isLive() to validate it before reading GPU bytes.
◆ updateSlot()
|
noexcept |
Upload size bytes starting at data into a slot.
Thin wrapper around QRhiResourceUpdateBatch::updateDynamicBuffer (for Dynamic-usage arenas) or uploadStaticBuffer (Static). Called by source nodes in their update() when their content changes — never per frame for unchanged data.
◆ uploadMeshStream()
|
noexcept |
Upload CPU bytes into a slab's stream. Thin wrapper around QRhiResourceUpdateBatch::uploadStaticBuffer at the slab's computed offset.
Member Data Documentation
◆ kMeshCapBytes
|
staticconstexpr |
The documentation for this class was generated from the following files:
- GpuResourceRegistry.hpp
- GpuResourceRegistry.cpp