Loading...
Searching...
No Matches
VertexFallbackPool.hpp
1#pragma once
2
3#include <Gfx/Graph/VertexFallbackDefaults.hpp>
4
5#include <ossia/detail/hash_map.hpp>
6
7#include <score_plugin_gfx_export.h>
8
9#include <cstdint>
10
11class QRhi;
12class QRhiBuffer;
13class QRhiResourceUpdateBatch;
14
15namespace score::gfx
16{
17
18// Shared pool of tiny PerInstance step_rate=1 vertex buffers used to
19// satisfy "REQUIRED: false" VERTEX_INPUTS whose upstream geometry does
20// not provide a matching attribute.
21//
22// A step_rate=1 PerInstance binding ADVANCES one element per instance;
23// it is not a broadcast. A buffer holding a single element therefore
24// reaches instance 0 only -- every later instance steps past the end and
25// fetches zero. Fixing that on the *binding*
26// is not portable: stepRate != 1 needs QRhi::CustomInstanceStepRate, which
27// OpenGL reports as false (qrhigles2.cpp) -- and a stride of 0 fails
28// Metal validation. So the constant is replicated inside the
29// buffer instead -- an entry holds `Entry::instances` copies of the
30// payload and instance i reads copy i. The pipeline's vertex input
31// layout fixes stride and classification, NOT buffer length, so growing
32// an entry never requires a pipeline rebuild.
33//
34// Lifetime-owned by the RenderList (same scope as GpuResourceRegistry).
35// Lookup key includes the format, stride, and a hash of the payload so
36// different DEFAULT values on the same semantic don't share a buffer.
37// A typical session touches ~5–10 distinct buckets; at the replication
38// floor that is tens of kilobytes, and a few hundred at the widest
39// payload the spec can carry.
40//
41// Not thread-safe: designed for single-threaded render-thread access.
42class SCORE_PLUGIN_GFX_EXPORT VertexFallbackPool
43{
44public:
45 // Replication floor, in instances. A draw whose instance count the CPU
46 // cannot see -- GPU-driven drawIndirect / drawIndirectCount, where the
47 // count lives in a buffer the host never reads -- never gets to call
48 // ensureInstances(), so every entry starts out covering this many
49 // instances. Draws whose count IS known grow past it on demand.
50 static constexpr uint32_t default_instances = 1024;
51
52 struct Entry
53 {
54 QRhiBuffer* buffer{}; // VertexBuffer | Immutable, `instances * stride` bytes
55 uint32_t stride{}; // matches spec.stride_bytes
56 int format{}; // matches spec.format (ossia::geometry::attribute::format)
57 uint32_t instances{}; // copies of the payload the buffer holds
58 };
59
60 VertexFallbackPool() = default;
62
64 VertexFallbackPool& operator=(const VertexFallbackPool&) = delete;
65
66 // Returns (and lazily creates) the shared buffer matching `spec`,
67 // holding at least max(instance_count, default_instances) copies of
68 // the payload. The first call per key allocates a QRhiBuffer and
69 // records an upload on `batch`; subsequent calls return the cached
70 // buffer, growing it first when this caller needs more instances than
71 // the cached one already covers.
72 //
73 // `rhi` and `batch` must be valid. The returned buffer is valid
74 // until release() is called.
75 Entry acquire(QRhi& rhi, QRhiResourceUpdateBatch& batch,
76 const VertexFallbackSpec& spec, uint32_t instance_count = 1);
77
78 // Grow the pooled entry whose buffer is `buffer` so that it covers
79 // `instance_count` instances, re-uploading the replicated payload.
80 // Called from the render path once the draw's instance count is known.
81 // The QRhiBuffer object is resized in place, so FallbackBindingPlan
82 // slots that cached the pointer stay valid and no pipeline is rebuilt.
83 //
84 // Returns false when `buffer` is not one of ours, or when the
85 // reallocation failed -- in which case the entry keeps the coverage it
86 // already had.
87 bool ensureInstances(QRhi& rhi, QRhiResourceUpdateBatch& batch,
88 QRhiBuffer* buffer, uint32_t instance_count);
89
90 // Destroy every cached buffer and clear the pool. Called by the
91 // owning RenderList on teardown.
92 void release();
93
94 // Diagnostic only.
95 std::size_t size() const noexcept { return m_entries.size(); }
96
97private:
98 struct Key
99 {
100 int format{};
101 uint32_t stride{};
102 uint64_t payload_hash{};
103
104 bool operator==(const Key& o) const noexcept
105 {
106 return format == o.format && stride == o.stride
107 && payload_hash == o.payload_hash;
108 }
109 };
110 struct KeyHash
111 {
112 std::size_t operator()(const Key& k) const noexcept
113 {
114 // Cheap mix — keys are already high-entropy via payload_hash.
115 return (std::size_t)(k.payload_hash
116 ^ ((uint64_t)k.format << 32)
117 ^ (uint64_t)k.stride);
118 }
119 };
120
121 // The payload is kept alongside the entry: growing a buffer means
122 // re-uploading the constant replicated N times, and the caller that
123 // knows the instance count (the render path) no longer has the spec.
124 struct Record
125 {
126 Entry entry;
127 VertexFallbackSpec spec;
128 };
129
130 // Resize `rec`'s buffer to hold max(instances, default_instances)
131 // copies of its payload, creating it if it does not exist yet.
132 // No-op (returns true) when the buffer already covers that many.
133 bool grow(QRhi& rhi, QRhiResourceUpdateBatch& batch, Record& rec,
134 uint32_t instances);
135
136 ossia::hash_map<Key, Record, KeyHash> m_entries;
137};
138
139} // namespace score::gfx
Definition VertexFallbackPool.hpp:43
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
Definition VertexFallbackPool.hpp:53
Definition VertexFallbackDefaults.hpp:22