Loading...
Searching...
No Matches
Mesh.hpp
1#pragma once
2
3#include <cstdint>
4#include <limits>
5#include <Process/ProcessFlags.hpp>
6
7#include <ossia/dataflow/geometry_port.hpp>
8#include <ossia/detail/small_vector.hpp>
9#include <span>
10
11#include <private/qrhi_p.h>
12
13#include <score_plugin_gfx_export.h>
14
15namespace score::gfx
16{
18{
19 QRhiBuffer* handle{};
20 int64_t byte_offset{};
21 int64_t byte_size{};
22
23 // Not Qt-gated: below Qt 6.12 there is no GPU indirect draw, but an indirect
24 // buffer tagged here still reaches the CPU-readback rung of the draw ladder
25 // (RenderedRawRasterPipelineNode::runInitialPasses).
26 enum class Usage : uint8_t
27 {
28 Direct,
29 IndirectDraw,
30 IndirectDrawIndexed
31 };
32 Usage usage{Usage::Direct};
33
34 // False for borrowed buffers — e.g., gpu_buffer handles the caller
35 // owns (scene preprocessor's MDI arena buffers, registry arena
36 // buffers). RenderList::release only `delete`s when owned=true; owners
37 // outside the RenderList's m_vertexBuffers destroy their own handles.
38 bool owned{true};
39
40 inline operator bool() const noexcept { return handle; }
41};
42// A QRhiBuffer's size is a quint32 (qrhi.h: `quint32 size() const`,
43// `setSize(quint32)`), while score carries geometry sizes as int64_t. Narrowing
44// is SILENT and catastrophic: 4 GiB + 4 KiB becomes 4 KiB, the allocation
45// succeeds, and every upload and draw afterwards addresses it as though it held
46// the original size.
47//
48// So ask before narrowing. A size that does not survive the round trip is not
49// expressible and the caller must refuse it rather than hand QRhi a number that
50// means something else.
51inline bool bufferSizeIsExpressible(int64_t byte_size) noexcept
52{
53 return byte_size >= 0 && byte_size <= int64_t(std::numeric_limits<quint32>::max());
54}
55
56// A draw command whose index/vertex count or instance count is zero paints
57// nothing. Every backend agrees on the outcome -- but they do not agree on how
58// to get there. Vulkan, D3D and GL treat it as a legal no-op; Metal's API
59// validation layer treats it as a programming error and ABORTS the process
60// ("indexCount(0) must be non-zero" / "instanceCount(0) must be non-zero"),
61// taking the whole app down.
62//
63// That matters because zero-count commands are not a pathology here, they are
64// the CONTRACT. RenderState::Caps requires indirect producers to keep the
65// command slots beyond their written count ZEROED, which is what makes the
66// full-capacity multi-draw rung (R2) equivalent to the GPU-count rung (R1).
67// When a producer publishes no "_indirect_draw_count" auxiliary there is no
68// count to clamp with, so the CPU-readback rung (R4) replays every capacity
69// slot verbatim -- dead zeroed ones included -- as explicit draw calls. On the
70// indirect rungs the GPU discards them silently; on the CPU rung they become
71// exactly the API call Metal refuses.
72//
73// So the rungs are only equivalent if the CPU rung skips what the GPU rung
74// would have discarded. Filter here.
75inline bool drawCommandPaints(const ossia::geometry::draw_command& cmd) noexcept
76{
77 return cmd.index_or_vertex_count > 0 && cmd.instance_count > 0;
78}
79
80// Observability for the filter above. Without it the filter is invisible on
81// every backend that is not Metal: the pixels are identical whether the dead
82// slots were skipped or issued as no-op draws, so a test on Linux could not
83// tell a working filter from a deleted one.
84//
85// Two mechanisms, because they answer different questions. The log line is for
86// a human reading a session's output and is warn-once, so it costs nothing in
87// a real render loop. The counter is for tests: warn-once is useless as an
88// assertion when a Catch2 process runs several backends in sequence -- the
89// first session consumes the only line and every later one sees nothing. A
90// test brackets a session and asserts the delta.
91SCORE_PLUGIN_GFX_EXPORT void noteZeroCountSlotsSkipped(int skipped) noexcept;
92SCORE_PLUGIN_GFX_EXPORT uint64_t zeroCountSlotsSkippedTotal() noexcept;
93
95{
96 ossia::small_vector<BufferView, 2> buffers;
97
98 // --- Multi-draw indirect state ---
99 // Always tracked regardless of Qt version. At draw time the path is:
100 // gpuIndirectSupported && indirectDrawBuffer → drawIndirect (GPU, Qt 6.12+)
101 // !gpuIndirectSupported && cpuDrawCommands → per-command drawIndexed loop
102 // neither → single drawIndexed
103 QRhiBuffer* indirectDrawBuffer{};
104 bool useIndirectDraw{false};
105 bool indirectDrawIndexed{false};
106 bool gpuIndirectSupported{false}; // set from RenderState::caps at init
108 bool baseInstanceSupported{true};
109 quint32 indirectDrawOffset{0};
110 quint32 indirectDrawCount{1};
111 quint32 indirectDrawStride{0};
112
113 // The uniform record the engine emits for one indirect command: 5 u32.
114 // Both layouts share it -- the non-indexed one is a native 4-word
115 // QRhiDrawIndirectCommand plus a trailing word, the indexed one is a native
116 // 5-word QRhiDrawIndexedIndirectCommand. See the libisf codegen.
117 static constexpr quint32 indirect_record_stride = 5 * sizeof(uint32_t);
118
119 // Turn the indirect path on. Use this rather than assigning the fields:
120 // setting useIndirectDraw without a stride is not a degradation, it is an
121 // ABORT -- indirectDrawStride defaults to 0 and QRhi asserts
122 // `stride >= sizeof(QRhi[Indexed]IndirectDrawCommand)` inside drawIndirect /
123 // drawIndexedIndirect, so the process dies the first time that mesh draws.
124 // The flag and the stride are not independently meaningful, so they are not
125 // independently settable.
126 void enableIndirectDraw(
127 QRhiBuffer* buffer, bool indexed, int64_t byte_size,
128 quint32 byte_offset = 0) noexcept
129 {
130 indirectDrawBuffer = buffer;
131 useIndirectDraw = true;
132 indirectDrawIndexed = indexed;
133 indirectDrawOffset = byte_offset;
134 indirectDrawStride = indirect_record_stride;
135 indirectDrawCount = quint32(byte_size / indirect_record_stride);
136 if(indirectDrawCount == 0)
137 indirectDrawCount = 1;
138 }
139
140 // --- GPU-decided draw count (Qt 6.13-era drawIndexedIndirectCount) ---
141 // When indirectCountBuffer is set AND gpuIndirectCountSupported, the draw
142 // reads its command count as a u32 out of this buffer at execution time,
143 // clamped to indirectDrawCount (which then acts as maxDrawCount == the
144 // command-slot capacity). Producers publish the buffer through the
145 // "_indirect_draw_count" auxiliary convention (CustomMesh picks it up).
146 //
147 // When the count rung is unavailable, correctness relies on the producer
148 // contract stated in RenderState::Caps: command slots beyond the written
149 // count stay zeroed, so the full-capacity multi-draw rung paints the same
150 // pixels. The CPU-readback fallback reads this buffer too and clamps
151 // cpuDrawCommands to the GPU-written count.
152 QRhiBuffer* indirectCountBuffer{};
153 quint32 indirectCountOffset{0};
154 bool gpuIndirectCountSupported{false}; // caps.drawIndirectCount &&
155 // drawIndirectCountUsable()
156 bool gpuIndirectMultiSupported{false}; // caps.drawIndirectMulti; false =>
157 // per-command drawCount=1 loop rung
158
159 // CPU-side draw commands. Populated either:
160 // a) directly by the producer (ScenePreprocessor has CPU data), or
161 // b) via GPU readback when the indirect buffer is GPU-generated (CSF)
162 // and gpuIndirectSupported is false.
163 ossia::small_vector<ossia::geometry::draw_command, 0> cpuDrawCommands;
164
165 // Readback result storage for the synchronous GPU→CPU fallback in
166 // RenderedRawRasterPipelineNode::runInitialPasses.
167 // Qt < 6.6 has a separate type for buffer readbacks.
168#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
169 QRhiReadbackResult readbackResult;
170 // Second slot for the 4-byte GPU-written draw count, read back alongside
171 // the commands so the CPU loop can clamp to it (see indirectCountBuffer).
172 QRhiReadbackResult countReadbackResult;
173#else
174 QRhiBufferReadbackResult readbackResult;
175 QRhiBufferReadbackResult countReadbackResult;
176#endif
177};
181struct SCORE_PLUGIN_GFX_EXPORT Mesh
182{
183public:
184 explicit Mesh();
185 virtual ~Mesh();
186
187 enum Flag
188 {
189 HasPosition = SCORE_FLAG(1),
190 HasTexCoord = SCORE_FLAG(2),
191 HasColor = SCORE_FLAG(3),
192 HasNormals = SCORE_FLAG(4),
193 HasTangents = SCORE_FLAG(5),
194 };
195 using Flags = QFlags<Flag>;
196
197 [[nodiscard]] virtual Flags flags() const noexcept = 0;
198
199 [[nodiscard]] virtual MeshBuffers init(QRhi& rhi) const noexcept = 0;
200
201 virtual void
202 update(QRhi& rhi, MeshBuffers& bufs, QRhiResourceUpdateBatch& cb) const noexcept
203 = 0;
204 virtual void preparePipeline(QRhiGraphicsPipeline& pip) const noexcept = 0;
205
206 // False when the mesh currently carries no sub-mesh: its vertex-input
207 // layout is empty and cannot satisfy a vertex shader that declares inputs.
208 [[nodiscard]] virtual bool hasGeometry() const noexcept { return true; }
209 virtual void draw(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept = 0;
210
212 virtual const char* defaultVertexShader() const noexcept = 0;
213
220 virtual const ossia::geometry* semanticGeometry() const noexcept { return nullptr; }
221
222 ossia::geometry_filter_list_ptr filters;
223
224 std::atomic_int64_t dirtyGeometryIndex{-1};
225
226 bool hasGeometryChanged(int64_t& renderer) const noexcept
227 {
228 int64_t res = dirtyGeometryIndex.load(std::memory_order_acquire);
229 if(renderer != res)
230 {
231 renderer = res;
232 return true;
233 }
234 return false;
235 }
236
237protected:
238 /*
239*/
240private:
241 Mesh(const Mesh&) = delete;
242 Mesh(Mesh&&) = delete;
243 Mesh& operator=(const Mesh&) = delete;
244 Mesh& operator=(Mesh&&) = delete;
245};
246
247Q_DECLARE_OPERATORS_FOR_FLAGS(Mesh::Flags);
248
249struct SCORE_PLUGIN_GFX_EXPORT BasicMesh : Mesh
250{
251 using Mesh::Mesh;
252 [[nodiscard]] virtual MeshBuffers init(QRhi& rhi) const noexcept override;
253 void update(
254 QRhi& rhi, MeshBuffers& bufs, QRhiResourceUpdateBatch& cb) const noexcept override;
255 void preparePipeline(QRhiGraphicsPipeline& pip) const noexcept override;
256 void draw(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
257 virtual void
258 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept = 0;
259
260 using pip = QRhiGraphicsPipeline;
261 pip::Topology topology = pip::Topology::TriangleStrip;
262 pip::CullMode cullMode = pip::CullMode::None;
263 pip::FrontFace frontFace = pip::FrontFace::CW;
264
265 ossia::small_vector<QRhiVertexInputBinding, 2> vertexBindings;
266 ossia::small_vector<QRhiVertexInputAttribute, 2> vertexAttributes;
267
268 std::span<const float> vertexArray;
269 int vertexCount{};
270};
271/*
272* @brief A dummy mesh for only accessing a gl_VertexId without caring about attributes
273*/
274struct SCORE_PLUGIN_GFX_EXPORT DummyMesh : BasicMesh
275{
276 explicit DummyMesh(int count);
277 [[nodiscard]] Flags flags() const noexcept override { return Flags{}; }
278 const char* defaultVertexShader() const noexcept override;
279 void
280 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
281 void draw(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
282};
283
287struct SCORE_PLUGIN_GFX_EXPORT PlainMesh : BasicMesh
288{
289 explicit PlainMesh(std::span<const float> vtx, int count);
290 [[nodiscard]] Flags flags() const noexcept override { return HasPosition; }
291 const char* defaultVertexShader() const noexcept override;
292 void
293 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
294};
295
299struct SCORE_PLUGIN_GFX_EXPORT TexturedMesh : BasicMesh
300{
301 explicit TexturedMesh(std::span<const float> vtx, int count);
302 [[nodiscard]] Flags flags() const noexcept override
303 {
304 return HasPosition | HasTexCoord;
305 }
306
307 const char* defaultVertexShader() const noexcept override;
308};
309
313struct SCORE_PLUGIN_GFX_EXPORT PlainTriangle final : PlainMesh
314{
315 static const constexpr float data[] = {-1, -1, 3, -1, -1, 3};
316
317 explicit PlainTriangle();
318 static const PlainTriangle& instance() noexcept;
319};
320
326struct SCORE_PLUGIN_GFX_EXPORT TexturedTriangle final : TexturedMesh
327{
328 static const constexpr float data[] = {// positions
329 -1, -1, 3, -1, -1, 3,
330 // tex coords
331 0, 0, 2, 0, 0, 2};
332 static const constexpr float flipped_y_data[] = {// positions
333 -1, -1, 3, -1, -1, 3,
334 // tex coords
335 0, 2, 2, 2, 0, 0};
336
337 explicit TexturedTriangle(bool flipped = false);
338
339 void
340 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
341};
342
347struct SCORE_PLUGIN_GFX_EXPORT TexturedQuad final : TexturedMesh
348{
349 static const constexpr float data[] = {// positions
350 -1, -1, +1, -1, -1, +1, +1, +1,
351 // tex coords
352 0, 0, 1, 0, 0, 1, 1, 1};
353
354 static const constexpr float flipped_y_data[] = {// positions
355 -1, -1, +1, -1, -1, +1, +1, +1,
356 // tex coords
357 0, 1, 1, 1, 0, 0, 1, 0};
358
359 explicit TexturedQuad(bool flipped = false);
360
361 void
362 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
363};
364
376SCORE_PLUGIN_GFX_EXPORT
378 const Mesh& mesh, const MeshBuffers& bufs, QRhiCommandBuffer& cb) noexcept;
379
380}
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
void drawMeshWithOptionalIndirect(const Mesh &mesh, const MeshBuffers &bufs, QRhiCommandBuffer &cb) noexcept
Draw a mesh, using indirect multi-draw when available in MeshBuffers.
Definition Mesh.cpp:314
Definition Mesh.hpp:250
Definition Mesh.hpp:18
Definition Mesh.hpp:275
Definition Mesh.hpp:95
Data model for meshes.
Definition Mesh.hpp:182
virtual const char * defaultVertexShader() const noexcept=0
A basic vertex shader that is going to work with this mesh.
A mesh with only position attributes.
Definition Mesh.hpp:288
A triangle mesh with only positions.
Definition Mesh.hpp:314
A mesh with positions and texture coordinates.
Definition Mesh.hpp:300
A quad mesh with positions and texture coordinates.
Definition Mesh.hpp:348
A triangle mesh with positions and texture coordinates.
Definition Mesh.hpp:327
Definition Uniforms.hpp:8