Loading...
Searching...
No Matches

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 Types

enum class  Arena : uint8_t {
  RawCamera , RawLight , RawTransform , Material ,
  Env , Count_
}
 
enum class  TextureChannel : uint8_t {
  BaseColor = 0 , MetalRough = 1 , Normal = 2 , Emissive = 3 ,
  Occlusion = 4 , Count_ = 5
}
 
enum class  MeshStream : uint8_t {
  Positions = 0 , Normals = 1 , Texcoords = 2 , Tangents = 3 ,
  Colors = 4 , Texcoords1 = 5 , Indices = 6 , Count_ = 7
}
 

Public Member Functions

 GpuResourceRegistry (const GpuResourceRegistry &)=delete
 
GpuResourceRegistryoperator= (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.
 
TextureChannelStatetextureChannel (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 TextureChannelStatetextureChannel (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.
 
MeshSlabacquireMeshSlab (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 Member Functions

static const char * textureChannelArrayName (TextureChannel ch) noexcept
 Shader-visible aux-texture name for a channel's static array (baseColorArray, metalRoughArray, normalArray, emissiveArray, occlusionArray).
 
static const char * textureChannelDynBaseName (TextureChannel ch) noexcept
 Shader-visible aux-texture name base for a channel's dynamic slots (baseColorDyn, metalRoughDyn, normalDyn, emissiveDyn, occlusionDyn). Full name is <base><slot_index>, slot_index < kMaxDynamicSlots.
 
static QRhiTexture::Flags textureChannelFlags (TextureChannel ch) noexcept
 QRhiTexture creation flags for a channel. sRGB channels (base color, emissive) get hardware sRGB→linear on sample; MR and normal stay linear.
 

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()

GpuResourceRegistry::MeshSlab * score::gfx::GpuResourceRegistry::acquireMeshSlab ( uint64_t  stable_id,
uint32_t  vertex_count,
uint32_t  index_count,
uint32_t  current_frame 
)
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()

QRhiBuffer * score::gfx::GpuResourceRegistry::buffer ( Arena  arena) const
noexcept

Buffer underlying an arena. Null until init().

Downstream consumers (preprocessor, rasterizer SRBs) bind this buffer with the slot offset + size from Slot.

◆ 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()

void score::gfx::GpuResourceRegistry::drainExpiredPendingReleases ( uint32_t  current_frame,
uint32_t  grace = 2 
)
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()

bool score::gfx::GpuResourceRegistry::isLive ( const ossia::gpu_slot_ref &  r) const
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()

bool score::gfx::GpuResourceRegistry::isLiveIn ( const ossia::gpu_slot_ref &  r,
Arena  expected 
) const
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()

uint32_t score::gfx::GpuResourceRegistry::meshSlabOffsetBytes ( const MeshSlab slab,
MeshStream  stream 
) const
noexcept

Byte offset of a stream within its backing buffer. Use directly as uploadStaticBuffer(buf, offset, size, data).

◆ meshStreamBuffer()

QRhiBuffer * score::gfx::GpuResourceRegistry::meshStreamBuffer ( MeshStream  s) const
noexcept

Backing QRhiBuffer for a stream. Stable pointer across the registry's lifetime (pre-sized, never grown).

◆ releaseMeshSlab()

void score::gfx::GpuResourceRegistry::releaseMeshSlab ( uint64_t  stable_id,
uint32_t  current_frame 
)
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()

int score::gfx::GpuResourceRegistry::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.

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()

void score::gfx::GpuResourceRegistry::sweepMeshSlabs ( uint32_t  current_frame,
uint32_t  grace = 2 
)
noexcept

Release slabs whose last_seen_frame < current_frame - grace. Grace defaults to 2 (covers FramesInFlight+1 on typical backends).

◆ sweepStaleDynamicTextureSlots()

void score::gfx::GpuResourceRegistry::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()

ossia::gpu_slot_ref score::gfx::GpuResourceRegistry::toOssiaRef ( const Slot slot) const
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()

void score::gfx::GpuResourceRegistry::updateSlot ( QRhiResourceUpdateBatch &  res,
const Slot slot,
const void *  data,
uint32_t  size 
)
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()

void score::gfx::GpuResourceRegistry::uploadMeshStream ( QRhiResourceUpdateBatch &  res,
const MeshSlab slab,
MeshStream  s,
const void *  data,
uint32_t  size 
)
noexcept

Upload CPU bytes into a slab's stream. Thin wrapper around QRhiResourceUpdateBatch::uploadStaticBuffer at the slab's computed offset.

Member Data Documentation

◆ kMeshCapBytes

constexpr uint32_t score::gfx::GpuResourceRegistry::kMeshCapBytes[(std::size_t) MeshStream::Count_]
staticconstexpr
Initial value:
= {
128u * 1024u * 1024u,
128u * 1024u * 1024u,
64u * 1024u * 1024u,
128u * 1024u * 1024u,
128u * 1024u * 1024u,
64u * 1024u * 1024u,
32u * 1024u * 1024u,
}

The documentation for this class was generated from the following files: