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_TEST_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{};
109 quint32 instanceMultiplier{1};
110 bool useIndirectDraw{false};
111 bool indirectDrawIndexed{false};
112 bool gpuIndirectSupported{false}; // set from RenderState::caps at init
114 bool baseInstanceSupported{true};
115 quint32 indirectDrawOffset{0};
116 quint32 indirectDrawCount{1};
117 quint32 indirectDrawStride{0};
118
119 // The uniform record the engine emits for one indirect command: 5 u32.
120 // Both layouts share it -- the non-indexed one is a native 4-word
121 // QRhiDrawIndirectCommand plus a trailing word, the indexed one is a native
122 // 5-word QRhiDrawIndexedIndirectCommand. See the libisf codegen.
123 static constexpr quint32 indirect_record_stride = 5 * sizeof(uint32_t);
124
125 // Turn the indirect path on. Use this rather than assigning the fields:
126 // setting useIndirectDraw without a stride is not a degradation, it is an
127 // ABORT -- indirectDrawStride defaults to 0 and QRhi asserts
128 // `stride >= sizeof(QRhi[Indexed]IndirectDrawCommand)` inside drawIndirect /
129 // drawIndexedIndirect, so the process dies the first time that mesh draws.
130 // The flag and the stride are not independently meaningful, so they are not
131 // independently settable.
132 void enableIndirectDraw(
133 QRhiBuffer* buffer, bool indexed, int64_t byte_size,
134 quint32 byte_offset = 0) noexcept
135 {
136 indirectDrawBuffer = buffer;
137 useIndirectDraw = true;
138 indirectDrawIndexed = indexed;
139 indirectDrawOffset = byte_offset;
140 indirectDrawStride = indirect_record_stride;
141 indirectDrawCount = quint32(byte_size / indirect_record_stride);
142 if(indirectDrawCount == 0)
143 indirectDrawCount = 1;
144 }
145
146 // --- GPU-decided draw count (Qt 6.13-era drawIndexedIndirectCount) ---
147 // When indirectCountBuffer is set AND gpuIndirectCountSupported, the draw
148 // reads its command count as a u32 out of this buffer at execution time,
149 // clamped to indirectDrawCount (which then acts as maxDrawCount == the
150 // command-slot capacity). Producers publish the buffer through the
151 // "_indirect_draw_count" auxiliary convention (CustomMesh picks it up).
152 //
153 // When the count rung is unavailable, the raw raster clamps a copy of the
154 // commands to the count on the GPU (clampedIndirectBuffer below), so the
155 // full-capacity multi-draw rung paints the same pixels; without compute it
156 // relies on the producer contract stated in RenderState::Caps: command
157 // slots beyond the written count stay zeroed. The CPU-readback fallback reads this buffer too and clamps
158 // cpuDrawCommands to the GPU-written count.
159 QRhiBuffer* indirectCountBuffer{};
160 quint32 indirectCountOffset{0};
166 QRhiBuffer* clampedIndirectBuffer{};
167 bool gpuIndirectCountSupported{false}; // caps.drawIndirectCount &&
168 // drawIndirectCountUsable(), for
169 // the pipeline that draws
170 bool gpuIndirectMultiSupported{false}; // caps.drawIndirectMulti; false =>
171 // per-command drawCount=1 loop rung
172
173 // CPU-side draw commands. Populated either:
174 // a) directly by the producer (ScenePreprocessor has CPU data), or
175 // b) via GPU readback when the indirect buffer is GPU-generated (CSF)
176 // and gpuIndirectSupported is false.
177 ossia::small_vector<ossia::geometry::draw_command, 0> cpuDrawCommands;
178
179 // Readback result storage for the synchronous GPU→CPU fallback in
180 // RenderedRawRasterPipelineNode::runInitialPasses.
181 // Qt < 6.6 has a separate type for buffer readbacks.
182#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
183 QRhiReadbackResult readbackResult;
184 // Second slot for the 4-byte GPU-written draw count, read back alongside
185 // the commands so the CPU loop can clamp to it (see indirectCountBuffer).
186 QRhiReadbackResult countReadbackResult;
187#else
188 QRhiBufferReadbackResult readbackResult;
189 QRhiBufferReadbackResult countReadbackResult;
190#endif
191};
195struct SCORE_PLUGIN_GFX_EXPORT Mesh
196{
197public:
198 explicit Mesh();
199 virtual ~Mesh();
200
201 enum Flag
202 {
203 HasPosition = SCORE_FLAG(1),
204 HasTexCoord = SCORE_FLAG(2),
205 HasColor = SCORE_FLAG(3),
206 HasNormals = SCORE_FLAG(4),
207 HasTangents = SCORE_FLAG(5),
208 };
209 using Flags = QFlags<Flag>;
210
211 [[nodiscard]] virtual Flags flags() const noexcept = 0;
212
213 [[nodiscard]] virtual MeshBuffers init(QRhi& rhi) const noexcept = 0;
214
215 virtual void
216 update(QRhi& rhi, MeshBuffers& bufs, QRhiResourceUpdateBatch& cb) const noexcept
217 = 0;
218 virtual void preparePipeline(QRhiGraphicsPipeline& pip) const noexcept = 0;
219
220 // False when the mesh currently carries no sub-mesh: its vertex-input
221 // layout is empty and cannot satisfy a vertex shader that declares inputs.
222 [[nodiscard]] virtual bool hasGeometry() const noexcept { return true; }
223 virtual void draw(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept = 0;
224
226 virtual const char* defaultVertexShader() const noexcept = 0;
227
234 virtual const ossia::geometry* semanticGeometry() const noexcept { return nullptr; }
235
236 ossia::geometry_filter_list_ptr filters;
237
238 std::atomic_int64_t dirtyGeometryIndex{-1};
239
240 bool hasGeometryChanged(int64_t& renderer) const noexcept
241 {
242 int64_t res = dirtyGeometryIndex.load(std::memory_order_acquire);
243 if(renderer != res)
244 {
245 renderer = res;
246 return true;
247 }
248 return false;
249 }
250
251protected:
252 /*
253*/
254private:
255 Mesh(const Mesh&) = delete;
256 Mesh(Mesh&&) = delete;
257 Mesh& operator=(const Mesh&) = delete;
258 Mesh& operator=(Mesh&&) = delete;
259};
260
261Q_DECLARE_OPERATORS_FOR_FLAGS(Mesh::Flags);
262
263struct SCORE_PLUGIN_GFX_EXPORT BasicMesh : Mesh
264{
265 using Mesh::Mesh;
266 [[nodiscard]] virtual MeshBuffers init(QRhi& rhi) const noexcept override;
267 void update(
268 QRhi& rhi, MeshBuffers& bufs, QRhiResourceUpdateBatch& cb) const noexcept override;
269 void preparePipeline(QRhiGraphicsPipeline& pip) const noexcept override;
270 void draw(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
271 virtual void
272 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept = 0;
273
274 using pip = QRhiGraphicsPipeline;
275 pip::Topology topology = pip::Topology::TriangleStrip;
276 pip::CullMode cullMode = pip::CullMode::None;
277 pip::FrontFace frontFace = pip::FrontFace::CW;
278
279 ossia::small_vector<QRhiVertexInputBinding, 2> vertexBindings;
280 ossia::small_vector<QRhiVertexInputAttribute, 2> vertexAttributes;
281
282 std::span<const float> vertexArray;
283 int vertexCount{};
284};
285/*
286* @brief A dummy mesh for only accessing a gl_VertexId without caring about attributes
287*/
288struct SCORE_PLUGIN_GFX_EXPORT DummyMesh : BasicMesh
289{
290 explicit DummyMesh(int count);
291 // When set, draw() issues indexCount uint32 indices from it instead of
292 // vertexCount vertices: gl_VertexIndex is then the index value, which lets a
293 // procedural vertex shader be drawn in another topology (see the VSA node).
294 QRhiBuffer* indexBuffer{};
295 int indexCount{};
296 [[nodiscard]] Flags flags() const noexcept override { return Flags{}; }
297 const char* defaultVertexShader() const noexcept override;
298 void
299 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
300 void draw(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
301};
302
306struct SCORE_PLUGIN_GFX_EXPORT PlainMesh : BasicMesh
307{
308 explicit PlainMesh(std::span<const float> vtx, int count);
309 [[nodiscard]] Flags flags() const noexcept override { return HasPosition; }
310 const char* defaultVertexShader() const noexcept override;
311 void
312 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
313};
314
318struct SCORE_PLUGIN_GFX_EXPORT TexturedMesh : BasicMesh
319{
320 explicit TexturedMesh(std::span<const float> vtx, int count);
321 [[nodiscard]] Flags flags() const noexcept override
322 {
323 return HasPosition | HasTexCoord;
324 }
325
326 const char* defaultVertexShader() const noexcept override;
327};
328
332struct SCORE_PLUGIN_GFX_EXPORT PlainTriangle final : PlainMesh
333{
334 static const constexpr float data[] = {-1, -1, 3, -1, -1, 3};
335
336 explicit PlainTriangle();
337 static const PlainTriangle& instance() noexcept;
338};
339
345struct SCORE_PLUGIN_GFX_EXPORT TexturedTriangle final : TexturedMesh
346{
347 static const constexpr float data[] = {// positions
348 -1, -1, 3, -1, -1, 3,
349 // tex coords
350 0, 0, 2, 0, 0, 2};
351 static const constexpr float flipped_y_data[] = {// positions
352 -1, -1, 3, -1, -1, 3,
353 // tex coords
354 0, 2, 2, 2, 0, 0};
355
356 explicit TexturedTriangle(bool flipped = false);
357
358 void
359 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
360};
361
366struct SCORE_PLUGIN_GFX_EXPORT TexturedQuad final : TexturedMesh
367{
368 static const constexpr float data[] = {// positions
369 -1, -1, +1, -1, -1, +1, +1, +1,
370 // tex coords
371 0, 0, 1, 0, 0, 1, 1, 1};
372
373 static const constexpr float flipped_y_data[] = {// positions
374 -1, -1, +1, -1, -1, +1, +1, +1,
375 // tex coords
376 0, 1, 1, 1, 0, 0, 1, 0};
377
378 explicit TexturedQuad(bool flipped = false);
379
380 void
381 setupBindings(const MeshBuffers& bufs, QRhiCommandBuffer& cb) const noexcept override;
382};
383
395SCORE_PLUGIN_GFX_EXPORT
397 const Mesh& mesh, const MeshBuffers& bufs, QRhiCommandBuffer& cb) noexcept;
398
399}
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:321
Definition Mesh.hpp:264
Definition Mesh.hpp:18
Definition Mesh.hpp:289
Definition Mesh.hpp:95
Data model for meshes.
Definition Mesh.hpp:196
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:307
A triangle mesh with only positions.
Definition Mesh.hpp:333
A mesh with positions and texture coordinates.
Definition Mesh.hpp:319
A quad mesh with positions and texture coordinates.
Definition Mesh.hpp:367
A triangle mesh with positions and texture coordinates.
Definition Mesh.hpp:346
Definition Uniforms.hpp:8