v2.0.0
Loading...
Searching...
No Matches
brainrenderer.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "brainrenderer.h"
18
19#include <rhi/qrhi.h>
27
29
30#include <QFile>
31#include <QDebug>
32#include <QImage>
33#include <QVector3D>
34#include <array>
35#include <limits>
36#include <cstring>
37#include <unordered_map>
38
39namespace DISP3DLIB
40{
41
42//=============================================================================================================
43// PIMPL
44//=============================================================================================================
45
46static constexpr int kNumShaderModes = 6; // Standard..ShowNormals
47
49{
50 void createResources(QRhi* rhi, QRhiRenderPassDescriptor* rp, int sampleCount);
51
52 // Offset of the next per-draw uniform slot, or -1 (with a warning) when the buffer is full or a pass is recorded
53 int claimUniformSlot(const char* caller);
54
55 void beginPass(QRhiCommandBuffer* cb, QRhiRenderTarget* rt, QRhiResourceUpdateBatch* updates);
56
57 // QRhi forbids resource updates inside a pass; true (with a warning) when a prepare call comes too late
58 bool insidePass(const char* caller) const;
59
60 // Copy the frame's uniforms written so far into the uniform buffer, growing it before the first pass
61 void flushUniforms(QRhiResourceUpdateBatch* u);
62
63 // ── GPU copies of renderable streams; renderables keep only CPU data ──
64 static constexpr int kMaxStreams = 4;
65 struct GpuMesh
66 {
67 std::weak_ptr<const void> owner;
68 std::array<std::unique_ptr<QRhiBuffer>, kMaxStreams> buffers;
69 std::array<quint64, kMaxStreams> generations{};
70 quint64 lastUsedFrame = 0;
71 };
72 GpuMesh& mesh(const RenderKey& key);
73 const GpuMesh* findMesh(const RenderKey& key) const;
74 QRhiBuffer* upload(GpuMesh& mesh, int slot, QRhiBuffer::UsageFlags usage, const GpuStream& stream, QRhiResourceUpdateBatch* u);
75 bool uploadSurface(BrainSurface* surface, QRhiResourceUpdateBatch* u);
76 bool uploadInstanced(const RenderKey& key, const InstancedStreams& streams, QRhiResourceUpdateBatch* u);
77 void bindSurface(QRhiCommandBuffer* cb, const GpuMesh& mesh);
78 void issueInstanced(QRhiCommandBuffer* cb, int uniformOffset, const RenderKey& key, const InstancedStreams& streams,
79 QRhiGraphicsPipeline* pipeline);
80 int writeInstancedUniforms(const SceneData& data, const char* caller);
81
82 QRhi* rhi = nullptr;
83 std::unordered_map<const void*, GpuMesh> meshes;
84 quint64 frameIndex = 0;
85
86 bool videoOverlayUsesDepth(const VideoOverlay* overlay) const
87 {
88 return overlay->isDepthEnabled() && overlay->hasDepthFrame() && videoOverlay.depthInitialized && videoOverlay.surfaceDepthPipeline;
89 }
90
91 std::unique_ptr<QRhiShaderResourceBindings> srb;
92
93 // Pipelines for each mode — indexed by ShaderMode enum (O(1) lookup)
94 std::array<std::unique_ptr<QRhiGraphicsPipeline>, kNumShaderModes> pipelines{};
95 std::array<std::unique_ptr<QRhiGraphicsPipeline>, kNumShaderModes> pipelinesBackColor{};
96 std::unique_ptr<QRhiGraphicsPipeline> electrodePipeline;
97
98 std::unique_ptr<QRhiBuffer> uniformBuffer;
99 // Whole bound block, not ubufAlignment(): that is 1 on GL, where slots would overlap
102 QByteArray uniformShadow; // The frame's per-draw uniforms, uploaded in one piece
104 bool overflowReported = false; // Reset per frame so the log gets one line, not one per skipped draw
105 bool passRecording = false;
106 bool passBegunThisFrame = false;
107
108 bool resourcesDirty = true;
109
110 // ── Dual render targets for multi-pass rendering ─────────────────
111 // Qt bakes load/store flags at create() time, so we need two separate
112 // render targets sharing the same color texture + depth buffer:
113 // - rtClear: clears framebuffer (first pass of each frame)
114 // - rtPreserve: preserves contents (subsequent passes)
115 // Validated in test_wasm_multi_pass on both Metal and WebGL.
116 std::unique_ptr<QRhiRenderBuffer> dsBuffer;
117 std::unique_ptr<QRhiTextureRenderTarget> rtClear;
118 std::unique_ptr<QRhiTextureRenderTarget> rtPreserve;
119 std::unique_ptr<QRhiRenderPassDescriptor> rpClear;
120 std::unique_ptr<QRhiRenderPassDescriptor> rpPreserve;
121 QSize rtSize;
122 QRhiTexture* rtColorTex = nullptr; // Track texture pointer for rebuild
123
124 // ── Generic video overlay ───────────────────────────────────────
125 // Camera-facing textured quad rendered last (depth test off) at the
126 // current focus point. Resources are created lazily on first use.
128 {
129 std::unique_ptr<QRhiGraphicsPipeline> pipeline;
130 std::unique_ptr<QRhiGraphicsPipeline> surfacePipeline;
131 std::unique_ptr<QRhiGraphicsPipeline> surfaceDepthPipeline; // POM-enhanced decal
132 std::unique_ptr<QRhiShaderResourceBindings> srb;
133 std::unique_ptr<QRhiShaderResourceBindings> srbDepth; // SRB with depth texture at binding 2
134 std::unique_ptr<QRhiBuffer> uniformBuffer;
135 std::unique_ptr<QRhiBuffer> vertexBuffer;
136 std::unique_ptr<QRhiBuffer> indexBuffer;
137 std::unique_ptr<QRhiTexture> texture;
138 std::unique_ptr<QRhiTexture> depthTexture;
139 std::unique_ptr<QRhiSampler> sampler;
140 std::unique_ptr<QRhiSampler> depthSampler; // mipmap-enabled for vertex displacement
145 quint64 uploadedFrameGen = std::numeric_limits<quint64>::max();
146 quint64 uploadedDepthFrameGen = std::numeric_limits<quint64>::max();
147 bool indexUploaded = false;
148 bool initialized = false;
149 bool depthInitialized = false;
150 };
152
153 // ── MRI slice rendering ─────────────────────────────────────────
154 // Up to 3 ortho slices (axial, coronal, sagittal) rendered as
155 // textured quads with depth test and alpha blending.
156 static constexpr int kMaxSliceSlots = 3;
158 {
159 std::unique_ptr<QRhiTexture> texture;
161 bool dirty = true; // needs texture re-upload
162 bool visible = false; // whether this slot has valid data
163 QVector<float> vertices; // 4 verts × (3 pos + 2 uv) = 20 floats
164 float opacity = 0.8f;
165 float windowCenter = 0.5f;
166 float windowWidth = 1.0f;
167 };
169 {
170 std::unique_ptr<QRhiGraphicsPipeline> pipeline;
171 std::unique_ptr<QRhiShaderResourceBindings> srb[kMaxSliceSlots];
172 std::unique_ptr<QRhiBuffer> uniformBuffer;
173 std::unique_ptr<QRhiBuffer> vertexBuffer[kMaxSliceSlots];
174 std::unique_ptr<QRhiBuffer> indexBuffer;
175 std::unique_ptr<QRhiSampler> sampler;
179 bool indexUploaded = false;
180 bool initialized = false;
181 };
183};
184
185//=============================================================================================================
186// Helpers
187//=============================================================================================================
188
189// Surface streams: geometry (position, normal), colour, annotation colour
190static QRhiVertexInputLayout surfaceInputLayout()
191{
192 QRhiVertexInputLayout il;
193 il.setBindings({{sizeof(VertexData)}, {sizeof(uint32_t)}, {sizeof(uint32_t)}});
194 il.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
195 {0, 1, QRhiVertexInputAttribute::Float3, 12},
196 {1, 2, QRhiVertexInputAttribute::UNormByte4, 0},
197 {2, 3, QRhiVertexInputAttribute::UNormByte4, 0}});
198 return il;
199}
200
201// Instanced glyphs: unit mesh (position, normal) plus a model matrix, RGBA colour and selected flag per instance
202static QRhiVertexInputLayout instancedInputLayout()
203{
204 QRhiVertexInputLayout il;
205 il.setBindings({{6 * sizeof(float)}, {21 * sizeof(float), QRhiVertexInputBinding::PerInstance}});
206 il.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
207 {0, 1, QRhiVertexInputAttribute::Float3, 3 * sizeof(float)},
208 {1, 2, QRhiVertexInputAttribute::Float4, 0},
209 {1, 3, QRhiVertexInputAttribute::Float4, 4 * sizeof(float)},
210 {1, 4, QRhiVertexInputAttribute::Float4, 8 * sizeof(float)},
211 {1, 5, QRhiVertexInputAttribute::Float4, 12 * sizeof(float)},
212 {1, 6, QRhiVertexInputAttribute::Float4, 16 * sizeof(float)},
213 {1, 7, QRhiVertexInputAttribute::Float, 20 * sizeof(float)}});
214 return il;
215}
216
217static QImage tightlyPackedRgba(const QImage& source)
218{
219 if (source.isNull())
220 return {};
221
222 QImage rgba = source.convertToFormat(QImage::Format_RGBA8888);
223 const qsizetype tightStride = qsizetype(rgba.width()) * 4;
224 if (rgba.bytesPerLine() == tightStride)
225 return rgba.copy();
226
227 QImage packed(rgba.size(), QImage::Format_RGBA8888);
228 for (int y = 0; y < rgba.height(); ++y) {
229 memcpy(packed.scanLine(y), rgba.constScanLine(y), size_t(tightStride));
230 }
231 return packed;
232}
233
234//=============================================================================================================
235// Uniform buffer layout constants — single source of truth for shader ↔ C++ interface
236//=============================================================================================================
237
238namespace
239{
240// Uniform buffer sizing: grows by doubling up to the maximum number of draws per frame
241constexpr int kInitialUniformSlots = 256;
242constexpr int kMaxUniformSlots = 16384;
243constexpr int kUniformBlockSize = 256; // Bound size per SRB dynamic slot (bytes)
244constexpr quint64 kEvictAfterFrames = 300; // GPU copies of renderables not drawn for this long are freed
245constexpr int kVideoOverlaySlots = 64; // Video overlay draws per frame
246constexpr quint32 kVideoQuadBytes = 4 * 5 * sizeof(float); // 4 verts × (3 pos + 2 uv)
247
248// Per-object uniform byte offsets (must match .vert shader layout).
249// Kept as documentation of the UBO layout even where not directly indexed.
250[[maybe_unused]] constexpr int kOffsetMVP = 0; // mat4 (64 bytes)
251[[maybe_unused]] constexpr int kOffsetCameraPos = 64; // vec3 (12 bytes)
252[[maybe_unused]] constexpr int kOffsetSelected = 76; // float
253[[maybe_unused]] constexpr int kOffsetLightDir = 80; // vec3 (12 bytes)
254[[maybe_unused]] constexpr int kOffsetTissueType = 92; // float
255[[maybe_unused]] constexpr int kOffsetLighting = 96; // float
256[[maybe_unused]] constexpr int kOffsetOverlayMode = 100; // float
257[[maybe_unused]] constexpr int kOffsetLabelActive = 104; // float
258[[maybe_unused]] constexpr int kOffsetSelectedLabel = 112; // vec4 (16 bytes)
259}
260
261//=============================================================================================================
262// DEFINE MEMBER METHODS
263//=============================================================================================================
264
265//=============================================================================================================
266
268: d(std::make_unique<Impl>())
269{
270}
271
272//=============================================================================================================
273
275
276//=============================================================================================================
277
278void BrainRenderer::initialize(QRhi* rhi, QRhiRenderPassDescriptor* rp, int sampleCount)
279{
280 if (d->resourcesDirty) {
281 d->createResources(rhi, rp, sampleCount);
282 }
283}
284
285//=============================================================================================================
286
288{
289 if (insidePass(caller))
290 return -1;
291 const int offset = currentUniformOffset;
292 // Once a pass has begun the buffer can no longer grow this frame
293 const qint64 capacity = passBegunThisFrame ? qint64(uniformBuffer->size()) : qint64(kMaxUniformSlots) * uniformSlotStride;
294 if (offset + uniformSlotStride > capacity) {
295 if (!overflowReported) {
296 qWarning("BrainRenderer: uniform buffer full at %s; further draws this frame are skipped", caller);
297 overflowReported = true;
298 }
299 return -1;
300 }
304 return offset;
305}
306
307//=============================================================================================================
308
309void BrainRenderer::Impl::flushUniforms(QRhiResourceUpdateBatch* u)
310{
312 return;
313 if (quint32(currentUniformOffset) > uniformBuffer->size()) {
314 int slotCount = int(uniformBuffer->size()) / uniformSlotStride;
315 while (slotCount * uniformSlotStride < currentUniformOffset)
316 slotCount *= 2;
317 uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, quint32(slotCount * uniformSlotStride)));
318 uniformBuffer->create();
319 srb->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, uniformBuffer.get(), kUniformBlockSize)});
320 srb->create();
322 }
323 u->updateDynamicBuffer(uniformBuffer.get(), quint32(flushedUniformBytes), quint32(currentUniformOffset - flushedUniformBytes),
324 uniformShadow.constData() + flushedUniformBytes);
326}
327
328//=============================================================================================================
329
331{
332 GpuMesh& m = meshes[key.id()];
333 // A new entry, or a stale one whose renderable died and whose address was reused
334 if (m.owner.expired()) {
335 m = GpuMesh{};
336 m.owner = key.token();
337 m.generations.fill(std::numeric_limits<quint64>::max());
338 }
340 return m;
341}
342
343//=============================================================================================================
344
346{
347 const auto it = meshes.find(key.id());
348 return (it == meshes.end() || it->second.owner.expired()) ? nullptr : &it->second;
349}
350
351//=============================================================================================================
352
353QRhiBuffer* BrainRenderer::Impl::upload(GpuMesh& m, int slot, QRhiBuffer::UsageFlags usage, const GpuStream& stream, QRhiResourceUpdateBatch* u)
354{
355 auto& buffer = m.buffers[slot];
356 if (!stream.data || stream.bytes == 0) {
357 buffer.reset();
358 return nullptr;
359 }
360 // Static buffers are updated in place; only a size change reallocates
361 if (!buffer || buffer->size() != stream.bytes) {
362 buffer.reset(rhi->newBuffer(QRhiBuffer::Static, usage, stream.bytes));
363 buffer->create();
364 m.generations[slot] = std::numeric_limits<quint64>::max();
365 }
366 if (m.generations[slot] != stream.generation) {
367 u->uploadStaticBuffer(buffer.get(), 0, stream.bytes, stream.data);
368 m.generations[slot] = stream.generation;
369 }
370 return buffer.get();
371}
372
373//=============================================================================================================
374
375bool BrainRenderer::Impl::uploadSurface(BrainSurface* surface, QRhiResourceUpdateBatch* u)
376{
377 if (!surface || surface->indexCount() == 0)
378 return false;
379 GpuMesh& m = mesh(surface->renderKey());
380 return upload(m, 0, QRhiBuffer::VertexBuffer, surface->geometryStream(), u) && upload(m, 1, QRhiBuffer::VertexBuffer, surface->colorStream(), u) && upload(m, 2, QRhiBuffer::VertexBuffer, surface->annotationStream(), u) && upload(m, 3, QRhiBuffer::IndexBuffer, surface->indexStream(), u);
381}
382
383//=============================================================================================================
384
385bool BrainRenderer::Impl::uploadInstanced(const RenderKey& key, const InstancedStreams& streams, QRhiResourceUpdateBatch* u)
386{
387 if (streams.indexCount <= 0 || streams.instanceCount <= 0)
388 return false;
389 GpuMesh& m = mesh(key);
390 return upload(m, 0, QRhiBuffer::VertexBuffer, streams.vertices, u) && upload(m, 1, QRhiBuffer::VertexBuffer, streams.instances, u) && upload(m, 2, QRhiBuffer::IndexBuffer, streams.indices, u);
391}
392
393//=============================================================================================================
394
395void BrainRenderer::Impl::bindSurface(QRhiCommandBuffer* cb, const GpuMesh& m)
396{
397 const QRhiCommandBuffer::VertexInput inputs[3] = {{m.buffers[0].get(), 0}, {m.buffers[1].get(), 0}, {m.buffers[2].get(), 0}};
398 cb->setVertexInput(0, 3, inputs, m.buffers[3].get(), 0, QRhiCommandBuffer::IndexUInt32);
399}
400
401//=============================================================================================================
402
403void BrainRenderer::Impl::createResources(QRhi* newRhi, QRhiRenderPassDescriptor* rp, int sampleCount)
404{
405 rhi = newRhi;
406 uniformSlotStride = rhi->ubufAligned(kUniformBlockSize);
407
408 if (!uniformBuffer) {
409 uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, kInitialUniformSlots * uniformSlotStride));
410 uniformBuffer->create();
411 }
412
413 // Create SRB
414 if (!srb) {
415 srb.reset(rhi->newShaderResourceBindings());
416 srb->setBindings({// Use dynamic offset for the uniform buffer.
417 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, uniformBuffer.get(), kUniformBlockSize)});
418 srb->create();
419 }
420
421 // Shader Loader
422 auto getShader = [](const QString& name) {
423 QFile f(name);
424 return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
425 };
426
427 // List of modes to initialize
428 QList<ShaderMode> modes = {Standard, Holographic, Anatomical, Dipole, XRay, ShowNormals};
429
430 for (ShaderMode mode : modes) {
431 QString vert = (mode == Holographic || mode == XRay) ? ":/holographic.vert.qsb" : (mode == Anatomical) ? ":/anatomical.vert.qsb"
432 : (mode == Dipole) ? ":/dipole.vert.qsb"
433 : (mode == ShowNormals) ? ":/shownormals.vert.qsb"
434 : ":/standard.vert.qsb";
435
436 QString frag = (mode == Holographic || mode == XRay) ? ":/holographic.frag.qsb" : (mode == Anatomical) ? ":/anatomical.frag.qsb"
437 : (mode == Dipole) ? ":/dipole.frag.qsb"
438 : (mode == ShowNormals) ? ":/shownormals.frag.qsb"
439 : ":/standard.frag.qsb";
440
441 QShader vS = getShader(vert);
442 QShader fS = getShader(frag);
443
444 if (!vS.isValid() || !fS.isValid()) {
445 qWarning() << "BrainRenderer: Could not load shaders for mode" << mode << vert << frag;
446 continue;
447 }
448
449 // Setup Pipeline
450 auto pipeline = std::unique_ptr<QRhiGraphicsPipeline>(rhi->newGraphicsPipeline());
451
452 QRhiGraphicsPipeline::TargetBlend blend;
453 if (mode == Holographic || mode == XRay) {
454 blend.enable = true;
455 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
456 blend.dstColor = QRhiGraphicsPipeline::One;
457 blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha;
458 blend.dstAlpha = QRhiGraphicsPipeline::One;
459 } else if (mode == Dipole) {
460 blend.enable = true;
461 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
462 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
463 blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha;
464 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
465 }
466
467 auto setup = [&](QRhiGraphicsPipeline* p, QRhiGraphicsPipeline::CullMode cull) {
468 p->setShaderStages({{QRhiShaderStage::Vertex, vS}, {QRhiShaderStage::Fragment, fS}});
469
470 p->setVertexInputLayout(mode == Dipole ? instancedInputLayout() : surfaceInputLayout());
471 p->setShaderResourceBindings(srb.get());
472 p->setRenderPassDescriptor(rp);
473 p->setSampleCount(sampleCount);
474 p->setCullMode(cull);
475 if (mode == Holographic) {
476 p->setTargetBlends({blend});
477 p->setDepthTest(true);
478 p->setDepthWrite(false);
479 } else if (mode == XRay) {
480 p->setTargetBlends({blend});
481 p->setDepthTest(false); // Disable Depth Test to see through head
482 p->setDepthWrite(false);
483 } else if (mode == Dipole) {
484 p->setTargetBlends({blend});
485 p->setCullMode(QRhiGraphicsPipeline::None);
486 p->setDepthTest(true);
487 p->setDepthWrite(false);
488 } else {
489 p->setDepthTest(true);
490 p->setDepthWrite(true);
491 }
492 p->setFlags(QRhiGraphicsPipeline::UsesScissor);
493 p->create();
494 };
495
496 if (mode == Holographic || mode == XRay) { // Handle XRay back-faces same as Holographic
497 auto pipelineBack = std::unique_ptr<QRhiGraphicsPipeline>(rhi->newGraphicsPipeline());
498 setup(pipelineBack.get(), QRhiGraphicsPipeline::Front);
499 pipelinesBackColor[mode] = std::move(pipelineBack);
500 setup(pipeline.get(), QRhiGraphicsPipeline::Back); // Front faces
501 } else {
502 // Culling: None (Double-sided) to be safe for FreeSurfer meshes
503 setup(pipeline.get(), QRhiGraphicsPipeline::None);
504 }
505 pipelines[mode] = std::move(pipeline);
506 }
507
508 // Electrodes: opaque and depth-written, unlike the translucent dipole glyphs
509 const QShader evS = getShader(QStringLiteral(":/electrode.vert.qsb"));
510 const QShader efS = getShader(QStringLiteral(":/electrode.frag.qsb"));
511 electrodePipeline.reset();
512 if (evS.isValid() && efS.isValid()) {
513 electrodePipeline.reset(rhi->newGraphicsPipeline());
514 electrodePipeline->setShaderStages({{QRhiShaderStage::Vertex, evS}, {QRhiShaderStage::Fragment, efS}});
515 electrodePipeline->setVertexInputLayout(instancedInputLayout());
516 electrodePipeline->setShaderResourceBindings(srb.get());
517 electrodePipeline->setRenderPassDescriptor(rp);
518 electrodePipeline->setSampleCount(sampleCount);
519 electrodePipeline->setCullMode(QRhiGraphicsPipeline::None);
520 electrodePipeline->setDepthTest(true);
521 electrodePipeline->setDepthWrite(true);
522 electrodePipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
523 electrodePipeline->create();
524 } else {
525 qWarning() << "BrainRenderer: Could not load the electrode shaders";
526 }
527
528 resourcesDirty = false;
529}
530
531//=============================================================================================================
532
533//=============================================================================================================
534
535void BrainRenderer::ensureRenderTargets(QRhi* rhi, QRhiTexture* colorTex, const QSize& pixelSize)
536{
537 // Rebuild when size changes OR when the backing texture changes
538 // (QRhiWidget may return a different colorTexture() each frame).
539 if (d->rtClear && d->rtSize == pixelSize && d->rtColorTex == colorTex)
540 return;
541
542 d->rtSize = pixelSize;
543 d->rtColorTex = colorTex;
544
545 // Shared depth-stencil buffer
546 d->dsBuffer.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize));
547 d->dsBuffer->create();
548
549 QRhiColorAttachment colorAtt(colorTex);
550 QRhiTextureRenderTargetDescription desc(colorAtt);
551 desc.setDepthStencilBuffer(d->dsBuffer.get());
552
553 // RT 1: Clearing (no preserve flags) — used for the first pass of each frame
554 d->rtClear.reset(rhi->newTextureRenderTarget(desc));
555 d->rpClear.reset(d->rtClear->newCompatibleRenderPassDescriptor());
556 d->rtClear->setRenderPassDescriptor(d->rpClear.get());
557 d->rtClear->create();
558
559 // RT 2: Preserving (load previous contents) — used for passes 2+
560 d->rtPreserve.reset(rhi->newTextureRenderTarget(desc,
561 QRhiTextureRenderTarget::PreserveColorContents | QRhiTextureRenderTarget::PreserveDepthStencilContents));
562 d->rpPreserve.reset(d->rtPreserve->newCompatibleRenderPassDescriptor());
563 d->rtPreserve->setRenderPassDescriptor(d->rpPreserve.get());
564 d->rtPreserve->create();
565}
566
567//=============================================================================================================
568
569QRhiRenderTarget* BrainRenderer::rtClear() const
570{
571 return d->rtClear.get();
572}
573
574QRhiRenderTarget* BrainRenderer::rtPreserve() const
575{
576 return d->rtPreserve.get();
577}
578
579//=============================================================================================================
580
582{
583 d->currentUniformOffset = 0;
584 d->flushedUniformBytes = 0;
585 d->overflowReported = false;
586 d->passBegunThisFrame = false;
587 d->videoOverlay.currentUniformOffset = 0;
588 d->sliceRes.currentUniformOffset = 0;
589
590 ++d->frameIndex;
591 for (auto it = d->meshes.begin(); it != d->meshes.end();) {
592 if (it->second.owner.expired() || d->frameIndex - it->second.lastUsedFrame > kEvictAfterFrames)
593 it = d->meshes.erase(it);
594 else
595 ++it;
596 }
597}
598
599//=============================================================================================================
600
602{
603 return int(d->meshes.size());
604}
605
606//=============================================================================================================
607
608void BrainRenderer::Impl::beginPass(QRhiCommandBuffer* cb, QRhiRenderTarget* rt, QRhiResourceUpdateBatch* updates)
609{
611 if (!updates)
612 updates = rhi->nextResourceUpdateBatch();
613 flushUniforms(updates);
614 }
615 cb->beginPass(rt, QColor(0, 0, 0), {1.0f, 0}, updates);
616 passRecording = true;
617 passBegunThisFrame = true;
618 const int w = rt->pixelSize().width();
619 const int h = rt->pixelSize().height();
620 cb->setViewport(QRhiViewport(0, 0, w, h));
621 cb->setScissor(QRhiScissor(0, 0, w, h));
622}
623
624//=============================================================================================================
625
626bool BrainRenderer::Impl::insidePass(const char* caller) const
627{
628 if (passRecording)
629 qWarning("BrainRenderer: %s called inside a render pass; resource updates must precede beginFrame()", caller);
630 return passRecording;
631}
632
633//=============================================================================================================
634
635void BrainRenderer::beginFrame(QRhiCommandBuffer* cb, QRhiResourceUpdateBatch* updates)
636{
637 d->beginPass(cb, d->rtClear.get(), updates);
638}
639
640//=============================================================================================================
641
642void BrainRenderer::beginPreservingPass(QRhiCommandBuffer* cb, QRhiResourceUpdateBatch* updates)
643{
644 d->beginPass(cb, d->rtPreserve.get(), updates);
645}
646
647//=============================================================================================================
648
649void BrainRenderer::endPass(QRhiCommandBuffer* cb)
650{
651 cb->endPass();
652 d->passRecording = false;
653}
654
655//=============================================================================================================
656// Video overlay rendering
657//=============================================================================================================
658
660 QRhiResourceUpdateBatch* u,
661 VideoOverlay* overlay)
662{
663 if (!overlay || !overlay->isEnabled())
664 return;
665 if (!overlay->hasFrame())
666 return;
667 if (!rhi || !u || d->insidePass("prepareVideoOverlay"))
668 return;
669
670 QImage frame = tightlyPackedRgba(overlay->frame());
671 if (frame.isNull())
672 return;
673
674 auto& k = d->videoOverlay;
675
676 // ── Lazy resource creation ──────────────────────────────────────
677 if (!k.initialized) {
678 // Shaders
679 QFile vFile(QStringLiteral(":/video_overlay.vert.qsb"));
680 QFile fFile(QStringLiteral(":/video_overlay.frag.qsb"));
681 if (!vFile.open(QIODevice::ReadOnly) || !fFile.open(QIODevice::ReadOnly)) {
682 qWarning() << "BrainRenderer: failed to open video overlay shaders";
683 return;
684 }
685 QShader vShader = QShader::fromSerialized(vFile.readAll());
686 QShader fShader = QShader::fromSerialized(fFile.readAll());
687 if (!vShader.isValid() || !fShader.isValid()) {
688 qWarning() << "BrainRenderer: invalid video overlay shaders";
689 return;
690 }
691
692 // Uniform buffer (mat4 + vec4 + 4 floats = 96 bytes, pad to 256)
693 k.uniformSlotStride = rhi->ubufAligned(kUniformBlockSize);
694 k.uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
695 QRhiBuffer::UniformBuffer,
696 kVideoOverlaySlots * k.uniformSlotStride));
697 k.uniformBuffer->create();
698
699 // Vertex buffer: one billboard quad per uniform slot — refilled each frame
700 k.vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
701 QRhiBuffer::VertexBuffer, kVideoOverlaySlots * kVideoQuadBytes));
702 k.vertexBuffer->create();
703
704 // Index buffer: 2 triangles, 6 indices — uploaded once
705 constexpr int kIbSize = 6 * sizeof(quint32);
706 k.indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable,
707 QRhiBuffer::IndexBuffer, kIbSize));
708 k.indexBuffer->create();
709
710 // Sampler (linear, clamp)
711 k.sampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear,
712 QRhiSampler::None,
713 QRhiSampler::ClampToEdge,
714 QRhiSampler::ClampToEdge));
715 k.sampler->create();
716
717 // Initial texture uses the first real frame size.
718 k.texture.reset(rhi->newTexture(QRhiTexture::RGBA8, frame.size()));
719 k.texture->create();
720 k.textureSize = frame.size();
721
722 // SRB binding: uniform @ 0, sampled image @ 1
723 k.srb.reset(rhi->newShaderResourceBindings());
724 k.srb->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
725 QRhiShaderResourceBinding::VertexStage |
726 QRhiShaderResourceBinding::FragmentStage,
727 k.uniformBuffer.get(), kUniformBlockSize),
728 QRhiShaderResourceBinding::sampledTexture(1,
729 QRhiShaderResourceBinding::FragmentStage,
730 k.texture.get(), k.sampler.get())});
731 k.srb->create();
732
733 // Pipeline (alpha-blended, depth-test off so it sits on top)
734 QRhiGraphicsPipeline::TargetBlend blend;
735 blend.enable = true;
736 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
737 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
738 blend.srcAlpha = QRhiGraphicsPipeline::One;
739 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
740
741 k.pipeline.reset(rhi->newGraphicsPipeline());
742 k.pipeline->setShaderStages({{QRhiShaderStage::Vertex, vShader},
743 {QRhiShaderStage::Fragment, fShader}});
744 QRhiVertexInputLayout il;
745 il.setBindings({{5 * sizeof(float)}});
746 il.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
747 {0, 1, QRhiVertexInputAttribute::Float2, 3 * sizeof(float)}});
748 k.pipeline->setVertexInputLayout(il);
749 k.pipeline->setShaderResourceBindings(k.srb.get());
750 k.pipeline->setRenderPassDescriptor(d->rtClear->renderPassDescriptor());
751 k.pipeline->setSampleCount(d->rtClear->sampleCount());
752 k.pipeline->setCullMode(QRhiGraphicsPipeline::None);
753 k.pipeline->setTargetBlends({blend});
754 k.pipeline->setDepthTest(false);
755 k.pipeline->setDepthWrite(false);
756 k.pipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
757 k.pipeline->create();
758
759 QFile dvFile(QStringLiteral(":/video_decal.vert.qsb"));
760 QFile dfFile(QStringLiteral(":/video_decal.frag.qsb"));
761 if (!dvFile.open(QIODevice::ReadOnly) || !dfFile.open(QIODevice::ReadOnly)) {
762 qWarning() << "BrainRenderer: failed to open video decal shaders";
763 return;
764 }
765 QShader dvShader = QShader::fromSerialized(dvFile.readAll());
766 QShader dfShader = QShader::fromSerialized(dfFile.readAll());
767 if (!dvShader.isValid() || !dfShader.isValid()) {
768 qWarning() << "BrainRenderer: invalid video decal shaders";
769 return;
770 }
771
772 QRhiGraphicsPipeline::TargetBlend decalBlend;
773 decalBlend.enable = true;
774 decalBlend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
775 decalBlend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
776 decalBlend.srcAlpha = QRhiGraphicsPipeline::One;
777 decalBlend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
778
779 k.surfacePipeline.reset(rhi->newGraphicsPipeline());
780 k.surfacePipeline->setShaderStages({{QRhiShaderStage::Vertex, dvShader},
781 {QRhiShaderStage::Fragment, dfShader}});
782 k.surfacePipeline->setVertexInputLayout(surfaceInputLayout());
783 k.surfacePipeline->setShaderResourceBindings(k.srb.get());
784 k.surfacePipeline->setRenderPassDescriptor(d->rtClear->renderPassDescriptor());
785 k.surfacePipeline->setSampleCount(d->rtClear->sampleCount());
786 k.surfacePipeline->setCullMode(QRhiGraphicsPipeline::None);
787 k.surfacePipeline->setTargetBlends({decalBlend});
788 k.surfacePipeline->setDepthTest(true);
789 k.surfacePipeline->setDepthWrite(false);
790 k.surfacePipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
791 k.surfacePipeline->create();
792
793 k.initialized = true;
794 }
795
796 // ── Lazy depth-enhanced pipeline creation ───────────────────────
797 if (overlay->isDepthEnabled() && !k.depthInitialized && k.initialized) {
798 QFile ddvFile(QStringLiteral(":/video_decal_depth.vert.qsb"));
799 QFile ddfFile(QStringLiteral(":/video_decal_depth.frag.qsb"));
800 if (ddvFile.open(QIODevice::ReadOnly) && ddfFile.open(QIODevice::ReadOnly)) {
801 QShader ddvShader = QShader::fromSerialized(ddvFile.readAll());
802 QShader ddfShader = QShader::fromSerialized(ddfFile.readAll());
803 if (ddvShader.isValid() && ddfShader.isValid()) {
804 // Mipmap-enabled sampler for depth texture (vertex shader
805 // samples a high LOD for smooth displacement on sparse meshes)
806 k.depthSampler.reset(rhi->newSampler(
807 QRhiSampler::Linear, QRhiSampler::Linear,
808 QRhiSampler::Linear, // mip filtering
809 QRhiSampler::ClampToEdge,
810 QRhiSampler::ClampToEdge));
811 k.depthSampler->create();
812
813 // 1x1 placeholder depth texture with mipmaps
814 k.depthTexture.reset(rhi->newTexture(
815 QRhiTexture::RGBA8, QSize(1, 1), 1, QRhiTexture::MipMapped));
816 k.depthTexture->create();
817 k.depthTextureSize = QSize(1, 1);
818
819 // SRB with depth texture at binding 2 (vertex + fragment)
820 k.srbDepth.reset(rhi->newShaderResourceBindings());
821 k.srbDepth->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
822 QRhiShaderResourceBinding::VertexStage |
823 QRhiShaderResourceBinding::FragmentStage,
824 k.uniformBuffer.get(), kUniformBlockSize),
825 QRhiShaderResourceBinding::sampledTexture(1,
826 QRhiShaderResourceBinding::FragmentStage,
827 k.texture.get(), k.sampler.get()),
828 QRhiShaderResourceBinding::sampledTexture(2,
829 QRhiShaderResourceBinding::VertexStage |
830 QRhiShaderResourceBinding::FragmentStage,
831 k.depthTexture.get(), k.depthSampler.get())});
832 k.srbDepth->create();
833
834 QRhiGraphicsPipeline::TargetBlend depthDecalBlend;
835 depthDecalBlend.enable = true;
836 depthDecalBlend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
837 depthDecalBlend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
838 depthDecalBlend.srcAlpha = QRhiGraphicsPipeline::One;
839 depthDecalBlend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
840
841 k.surfaceDepthPipeline.reset(rhi->newGraphicsPipeline());
842 k.surfaceDepthPipeline->setShaderStages({{QRhiShaderStage::Vertex, ddvShader},
843 {QRhiShaderStage::Fragment, ddfShader}});
844 k.surfaceDepthPipeline->setVertexInputLayout(surfaceInputLayout());
845 k.surfaceDepthPipeline->setShaderResourceBindings(k.srbDepth.get());
846 k.surfaceDepthPipeline->setRenderPassDescriptor(d->rtClear->renderPassDescriptor());
847 k.surfaceDepthPipeline->setSampleCount(d->rtClear->sampleCount());
848 k.surfaceDepthPipeline->setCullMode(QRhiGraphicsPipeline::None);
849 k.surfaceDepthPipeline->setTargetBlends({depthDecalBlend});
850 k.surfaceDepthPipeline->setDepthTest(false); // inward-displaced vertices must not be culled by brain surface z
851 k.surfaceDepthPipeline->setDepthWrite(false);
852 k.surfaceDepthPipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
853 k.surfaceDepthPipeline->create();
854 k.depthInitialized = true;
855 }
856 }
857 }
858
859 // ── Index buffer (one-shot upload) ──────────────────────────────
860 if (!k.indexUploaded) {
861 const quint32 idx[6] = {0, 1, 2, 2, 1, 3};
862 u->uploadStaticBuffer(k.indexBuffer.get(), idx);
863 k.indexUploaded = true;
864 }
865
866 // ── Texture (re-create on size change, re-upload on new frame) ──
867 if (frame.size() != k.textureSize) {
868 k.texture.reset(rhi->newTexture(QRhiTexture::RGBA8, frame.size()));
869 k.texture->create();
870 k.textureSize = frame.size();
871 // Rebuild SRB to point at the new texture
872 k.srb->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
873 QRhiShaderResourceBinding::VertexStage |
874 QRhiShaderResourceBinding::FragmentStage,
875 k.uniformBuffer.get(), kUniformBlockSize),
876 QRhiShaderResourceBinding::sampledTexture(1,
877 QRhiShaderResourceBinding::FragmentStage,
878 k.texture.get(), k.sampler.get())});
879 k.srb->create();
880 // Also rebuild depth SRB if it exists
881 if (k.srbDepth && k.depthTexture) {
882 k.srbDepth->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
883 QRhiShaderResourceBinding::VertexStage |
884 QRhiShaderResourceBinding::FragmentStage,
885 k.uniformBuffer.get(), kUniformBlockSize),
886 QRhiShaderResourceBinding::sampledTexture(1,
887 QRhiShaderResourceBinding::FragmentStage,
888 k.texture.get(), k.sampler.get()),
889 QRhiShaderResourceBinding::sampledTexture(2,
890 QRhiShaderResourceBinding::VertexStage |
891 QRhiShaderResourceBinding::FragmentStage,
892 k.depthTexture.get(), k.depthSampler.get())});
893 k.srbDepth->create();
894 }
895 k.uploadedFrameGen = std::numeric_limits<quint64>::max();
896 }
897 if (overlay->frameGeneration() != k.uploadedFrameGen) {
898 QRhiTextureSubresourceUploadDescription sub(frame);
899 QRhiTextureUploadDescription desc({0, 0, sub});
900 u->uploadTexture(k.texture.get(), desc);
901 k.uploadedFrameGen = overlay->frameGeneration();
902 }
903
904 // ── Depth texture upload ────────────────────────────────────────
905 if (overlay->isDepthEnabled() && overlay->hasDepthFrame() && k.depthInitialized) {
906 QImage depthFrame = tightlyPackedRgba(overlay->depthFrame());
907 if (!depthFrame.isNull()) {
908 if (depthFrame.size() != k.depthTextureSize) {
909 k.depthTexture.reset(rhi->newTexture(
910 QRhiTexture::RGBA8, depthFrame.size(), 1, QRhiTexture::MipMapped));
911 k.depthTexture->create();
912 k.depthTextureSize = depthFrame.size();
913 // Rebuild depth SRB with new depth texture
914 k.srbDepth->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
915 QRhiShaderResourceBinding::VertexStage |
916 QRhiShaderResourceBinding::FragmentStage,
917 k.uniformBuffer.get(), kUniformBlockSize),
918 QRhiShaderResourceBinding::sampledTexture(1,
919 QRhiShaderResourceBinding::FragmentStage,
920 k.texture.get(), k.sampler.get()),
921 QRhiShaderResourceBinding::sampledTexture(2,
922 QRhiShaderResourceBinding::VertexStage |
923 QRhiShaderResourceBinding::FragmentStage,
924 k.depthTexture.get(), k.depthSampler.get())});
925 k.srbDepth->create();
926 k.uploadedDepthFrameGen = std::numeric_limits<quint64>::max();
927 }
928 if (overlay->depthFrameGeneration() != k.uploadedDepthFrameGen) {
929 QRhiTextureSubresourceUploadDescription depthSub(depthFrame);
930 QRhiTextureUploadDescription depthDesc({0, 0, depthSub});
931 u->uploadTexture(k.depthTexture.get(), depthDesc);
932 u->generateMips(k.depthTexture.get());
933 k.uploadedDepthFrameGen = overlay->depthFrameGeneration();
934 }
935 }
936 }
937}
938
939int BrainRenderer::prepareVideoOverlayDraw(QRhiResourceUpdateBatch* u,
940 const SceneData& data,
941 VideoOverlay* overlay,
942 BrainSurface* surface)
943{
944 if (!overlay || !overlay->isEnabled() || !overlay->hasFrame())
945 return -1;
946
947 auto& k = d->videoOverlay;
948 if (!k.initialized || (surface && !k.surfacePipeline))
949 return -1;
950 if (d->insidePass("prepareVideoOverlayDraw"))
951 return -1;
952 if (surface && !d->uploadSurface(surface, u))
953 return -1;
954
955 const int uniformOffset = k.currentUniformOffset;
956 if (static_cast<quint32>(uniformOffset + kUniformBlockSize) > k.uniformBuffer->size())
957 return -1;
958 k.currentUniformOffset += k.uniformSlotStride;
959
960 if (!surface) {
961 // ── Billboard the quad to face the camera ──────────────────────
962 const QVector3D centre = overlay->focusPosition();
963 const float sizeM = overlay->sizeMeters();
964
965 // Preserve the video frame's aspect ratio: sizeMeters controls the
966 // width, and the height is derived from the frame dimensions.
967 const QImage& frm = overlay->frame();
968 const float aspect = (frm.height() > 0)
969 ? static_cast<float>(frm.width()) / frm.height()
970 : 1.0f;
971 const float halfW = 0.5f * sizeM;
972 const float halfH = (aspect > 0.0f) ? halfW / aspect : halfW;
973
974 QVector3D viewDir = centre - data.cameraPos;
975 if (viewDir.lengthSquared() < 1e-12f)
976 viewDir = QVector3D(0, 0, -1);
977 viewDir.normalize();
978
979 // Use the upHint (tracker→objective axis) when available so that
980 // the quad's long edge is perpendicular to the optical axis.
981 const QVector3D hint = overlay->upHint();
982 QVector3D up;
983 if (hint.lengthSquared() > 1e-8f) {
984 // Project the hint onto the plane perpendicular to viewDir
985 up = (hint - QVector3D::dotProduct(hint, viewDir) * viewDir).normalized();
986 if (up.lengthSquared() < 1e-8f)
987 up = QVector3D(0.0f, 0.0f, 1.0f);
988 } else {
989 QVector3D worldUp(0.0f, 0.0f, 1.0f);
990 if (std::abs(QVector3D::dotProduct(viewDir, worldUp)) > 0.95f)
991 worldUp = QVector3D(0.0f, 1.0f, 0.0f);
992 up = QVector3D::crossProduct(
993 QVector3D::crossProduct(viewDir, worldUp), viewDir)
994 .normalized();
995 }
996 QVector3D right = QVector3D::crossProduct(viewDir, up).normalized();
997
998 const QVector3D c00 = centre - right * halfW - up * halfH;
999 const QVector3D c10 = centre + right * halfW - up * halfH;
1000 const QVector3D c01 = centre - right * halfW + up * halfH;
1001 const QVector3D c11 = centre + right * halfW + up * halfH;
1002
1003 const float verts[4 * 5] = {
1004 c00.x(), c00.y(), c00.z(), 0.0f, 1.0f, // image y is flipped vs uv
1005 c10.x(), c10.y(), c10.z(), 1.0f, 1.0f,
1006 c01.x(), c01.y(), c01.z(), 0.0f, 0.0f,
1007 c11.x(), c11.y(), c11.z(), 1.0f, 0.0f};
1008 // Each slot has its own quad: every pane faces its own camera
1009 static_assert(sizeof(verts) == kVideoQuadBytes);
1010 u->updateDynamicBuffer(k.vertexBuffer.get(), quint32(uniformOffset / k.uniformSlotStride) * kVideoQuadBytes, sizeof(verts), verts);
1011
1012 // ── Uniforms (mat4 mvp, vec4 borderColor, float opacity, 3 pad) ─
1013 struct
1014 {
1015 float mvp[16];
1016 float borderColor[4];
1017 float opacity;
1018 float pad0, pad1, pad2;
1019 } ub;
1020 memcpy(ub.mvp, data.mvp.constData(), 64);
1021 ub.borderColor[0] = 0.0f;
1022 ub.borderColor[1] = 0.85f;
1023 ub.borderColor[2] = 1.0f;
1024 ub.borderColor[3] = 1.0f;
1025 ub.opacity = overlay->opacity();
1026 ub.pad0 = ub.pad1 = ub.pad2 = 0.0f;
1027 u->updateDynamicBuffer(k.uniformBuffer.get(), uniformOffset, sizeof(ub), &ub);
1028 return uniformOffset;
1029 }
1030
1031 // Approximate the local scalp normal from the focus position vector.
1032 QVector3D localNormal = overlay->focusPosition();
1033 if (localNormal.lengthSquared() < 1e-12f) {
1034 localNormal = QVector3D(0.0f, 0.0f, 1.0f);
1035 }
1036 localNormal.normalize();
1037
1038 // Use the tracker-derived up hint when available — this keeps the
1039 // decal orientation locked to the physical microscope and avoids
1040 // the 180° flip singularity that occurs with a fixed reference axis.
1041 QVector3D referenceUp = overlay->upHint();
1042 if (referenceUp.lengthSquared() < 1e-6f) {
1043 // Fallback when no tracker up is available.
1044 referenceUp = QVector3D(0.0f, 0.0f, 1.0f);
1045 if (std::abs(QVector3D::dotProduct(localNormal, referenceUp)) > 0.95f) {
1046 referenceUp = QVector3D(1.0f, 0.0f, 0.0f);
1047 }
1048 }
1049 // Orthogonalise against the surface normal so the frame is tangent.
1050 referenceUp = (referenceUp - QVector3D::dotProduct(referenceUp, localNormal) * localNormal);
1051 if (referenceUp.lengthSquared() < 1e-12f) {
1052 referenceUp = QVector3D(0.0f, 0.0f, 1.0f);
1053 }
1054 referenceUp.normalize();
1055
1056 const QVector3D axisV = referenceUp;
1057 const QVector3D axisU = QVector3D::crossProduct(axisV, localNormal).normalized();
1058
1059 const bool useDepth = d->videoOverlayUsesDepth(overlay);
1060
1061 // Uniform block — the depth-enhanced shader has an extra vec4 depthParams
1062 // but the base 7×vec4 layout (112 bytes) still fits within kUniformBlockSize
1063 // (256 bytes) even with the extra vec4 (128 bytes total).
1064 struct
1065 {
1066 float mvp[16];
1067 float focusAndSize[4];
1068 float axisUAndOpacity[4];
1069 float axisVAndOffset[4];
1070 float axisNAndDepth[4];
1071 float cameraPosAndFacing[4];
1072 float borderColor[4];
1073 float depthParams[4];
1074 } ub;
1075 memcpy(ub.mvp, data.mvp.constData(), 64);
1076 ub.focusAndSize[0] = overlay->focusPosition().x();
1077 ub.focusAndSize[1] = overlay->focusPosition().y();
1078 ub.focusAndSize[2] = overlay->focusPosition().z();
1079 ub.focusAndSize[3] = overlay->sizeMeters();
1080 ub.axisUAndOpacity[0] = axisU.x();
1081 ub.axisUAndOpacity[1] = axisU.y();
1082 ub.axisUAndOpacity[2] = axisU.z();
1083 ub.axisUAndOpacity[3] = overlay->opacity();
1084 ub.axisVAndOffset[0] = axisV.x();
1085 ub.axisVAndOffset[1] = axisV.y();
1086 ub.axisVAndOffset[2] = axisV.z();
1087 ub.axisVAndOffset[3] = 0.00045f;
1088 ub.axisNAndDepth[0] = localNormal.x();
1089 ub.axisNAndDepth[1] = localNormal.y();
1090 ub.axisNAndDepth[2] = localNormal.z();
1091 ub.axisNAndDepth[3] = std::max(0.015f, overlay->sizeMeters() * 0.45f);
1092 ub.cameraPosAndFacing[0] = data.cameraPos.x();
1093 ub.cameraPosAndFacing[1] = data.cameraPos.y();
1094 ub.cameraPosAndFacing[2] = data.cameraPos.z();
1095 const QImage& decalFrame = overlay->frame();
1096 ub.cameraPosAndFacing[3] = (decalFrame.height() > 0)
1097 ? static_cast<float>(decalFrame.width()) / decalFrame.height()
1098 : 1.0f;
1099 ub.borderColor[0] = 0.0f;
1100 ub.borderColor[1] = 0.85f;
1101 ub.borderColor[2] = 1.0f;
1102 ub.borderColor[3] = 1.0f;
1103 ub.depthParams[0] = useDepth ? overlay->depthScale() : 0.0f;
1104 ub.depthParams[1] = useDepth ? static_cast<float>(overlay->depthSteps()) : 0.0f;
1105 ub.depthParams[2] = useDepth ? 1.0f : 0.0f;
1106 ub.depthParams[3] = 0.0f;
1107 u->updateDynamicBuffer(k.uniformBuffer.get(), uniformOffset, sizeof(ub), &ub);
1108 return uniformOffset;
1109}
1110
1111//=============================================================================================================
1112
1113void BrainRenderer::issueVideoOverlayDraw(QRhiCommandBuffer* cb,
1114 VideoOverlay* overlay,
1115 BrainSurface* surface,
1116 int uniformOffset)
1117{
1118 if (!overlay || uniformOffset < 0)
1119 return;
1120
1121 auto& k = d->videoOverlay;
1122 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
1123 if (surface) {
1124 const Impl::GpuMesh* m = d->findMesh(surface->renderKey());
1125 if (!m)
1126 return;
1127 const bool useDepth = d->videoOverlayUsesDepth(overlay);
1128 cb->setGraphicsPipeline(useDepth ? k.surfaceDepthPipeline.get() : k.surfacePipeline.get());
1129 cb->setShaderResources(useDepth ? k.srbDepth.get() : k.srb.get(), 1, &srbOffset);
1130 d->bindSurface(cb, *m);
1131 cb->drawIndexed(surface->indexCount());
1132 return;
1133 }
1134
1135 cb->setGraphicsPipeline(k.pipeline.get());
1136 cb->setShaderResources(k.srb.get(), 1, &srbOffset);
1137 const QRhiCommandBuffer::VertexInput vbuf(k.vertexBuffer.get(), quint32(uniformOffset / k.uniformSlotStride) * kVideoQuadBytes);
1138 cb->setVertexInput(0, 1, &vbuf, k.indexBuffer.get(), 0, QRhiCommandBuffer::IndexUInt32);
1139 cb->drawIndexed(6);
1140}
1141
1142//=============================================================================================================
1143// MRI slice rendering
1144//=============================================================================================================
1145
1147 QRhiResourceUpdateBatch* u,
1149 int slotIndex)
1150{
1151 if (!rhi || !u || d->insidePass("prepareSlice"))
1152 return;
1153 if (slotIndex < 0 || slotIndex >= Impl::kMaxSliceSlots)
1154 return;
1155
1156 auto& k = d->sliceRes;
1157 auto& sliceSlot = k.sliceSlots[slotIndex];
1158
1159 if (!slice) {
1160 sliceSlot.visible = false;
1161 return;
1162 }
1163
1164 const QImage& img = slice->image();
1165 if (img.isNull()) {
1166 sliceSlot.visible = false;
1167 return;
1168 }
1169
1170 // ── Lazy one-time resource creation ─────────────────────────────
1171 if (!k.initialized) {
1172 QFile vFile(QStringLiteral(":/slice.vert.qsb"));
1173 QFile fFile(QStringLiteral(":/slice.frag.qsb"));
1174 if (!vFile.open(QIODevice::ReadOnly) || !fFile.open(QIODevice::ReadOnly)) {
1175 qWarning() << "BrainRenderer: failed to open MRI slice shaders";
1176 return;
1177 }
1178 QShader vShader = QShader::fromSerialized(vFile.readAll());
1179 QShader fShader = QShader::fromSerialized(fFile.readAll());
1180 if (!vShader.isValid() || !fShader.isValid()) {
1181 qWarning() << "BrainRenderer: invalid MRI slice shaders";
1182 return;
1183 }
1184
1185 k.uniformSlotStride = rhi->ubufAligned(kUniformBlockSize);
1186 k.uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1187 QRhiBuffer::UniformBuffer,
1188 64 * k.uniformSlotStride));
1189 k.uniformBuffer->create();
1190
1191 // Index buffer (shared by all 3 slots): 2 triangles = 6 indices
1192 k.indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable,
1193 QRhiBuffer::IndexBuffer,
1194 6 * sizeof(quint32)));
1195 k.indexBuffer->create();
1196
1197 k.sampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear,
1198 QRhiSampler::None,
1199 QRhiSampler::ClampToEdge,
1200 QRhiSampler::ClampToEdge));
1201 k.sampler->create();
1202
1203 // Per-slot resources: vertex buffer + texture + SRB
1204 for (int i = 0; i < Impl::kMaxSliceSlots; ++i) {
1205 constexpr int kVbSize = 4 * 5 * sizeof(float); // 4 verts × (3 pos + 2 uv)
1206 k.vertexBuffer[i].reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1207 QRhiBuffer::VertexBuffer, kVbSize));
1208 k.vertexBuffer[i]->create();
1209
1210 // 1×1 placeholder texture — real size set on first data
1211 k.sliceSlots[i].texture.reset(rhi->newTexture(QRhiTexture::R8, QSize(1, 1)));
1212 k.sliceSlots[i].texture->create();
1213 k.sliceSlots[i].textureSize = QSize(1, 1);
1214
1215 k.srb[i].reset(rhi->newShaderResourceBindings());
1216 k.srb[i]->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
1217 QRhiShaderResourceBinding::VertexStage |
1218 QRhiShaderResourceBinding::FragmentStage,
1219 k.uniformBuffer.get(), kUniformBlockSize),
1220 QRhiShaderResourceBinding::sampledTexture(1,
1221 QRhiShaderResourceBinding::FragmentStage,
1222 k.sliceSlots[i].texture.get(), k.sampler.get())});
1223 k.srb[i]->create();
1224 }
1225
1226 // Pipeline: alpha-blended, depth-test on, depth-write off
1227 QRhiGraphicsPipeline::TargetBlend blend;
1228 blend.enable = true;
1229 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
1230 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1231 blend.srcAlpha = QRhiGraphicsPipeline::One;
1232 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1233
1234 k.pipeline.reset(rhi->newGraphicsPipeline());
1235 k.pipeline->setShaderStages({{QRhiShaderStage::Vertex, vShader},
1236 {QRhiShaderStage::Fragment, fShader}});
1237 QRhiVertexInputLayout il;
1238 il.setBindings({{5 * sizeof(float)}});
1239 il.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
1240 {0, 1, QRhiVertexInputAttribute::Float2, 3 * sizeof(float)}});
1241 k.pipeline->setVertexInputLayout(il);
1242 k.pipeline->setShaderResourceBindings(k.srb[0].get());
1243 k.pipeline->setRenderPassDescriptor(d->rtClear->renderPassDescriptor());
1244 k.pipeline->setSampleCount(d->rtClear->sampleCount());
1245 k.pipeline->setCullMode(QRhiGraphicsPipeline::None);
1246 k.pipeline->setTargetBlends({blend});
1247 k.pipeline->setDepthTest(true);
1248 k.pipeline->setDepthWrite(false);
1249 k.pipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
1250 k.pipeline->create();
1251
1252 k.initialized = true;
1253 }
1254
1255 // ── Index buffer (one-shot upload) ──────────────────────────────
1256 if (!k.indexUploaded) {
1257 const quint32 idx[6] = {0, 1, 2, 2, 1, 3};
1258 u->uploadStaticBuffer(k.indexBuffer.get(), idx);
1259 k.indexUploaded = true;
1260 }
1261
1262 // ── Texture (re-create on size change, upload data) ─────────────
1263 // Convert to R8 (grayscale) for the slice shader
1264 QImage gray = img.convertToFormat(QImage::Format_Grayscale8);
1265 const QSize imgSize = gray.size();
1266
1267 if (imgSize != sliceSlot.textureSize) {
1268 sliceSlot.texture.reset(rhi->newTexture(QRhiTexture::R8, imgSize));
1269 sliceSlot.texture->create();
1270 sliceSlot.textureSize = imgSize;
1271 // Rebuild SRB to point at new texture
1272 k.srb[slotIndex]->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
1273 QRhiShaderResourceBinding::VertexStage |
1274 QRhiShaderResourceBinding::FragmentStage,
1275 k.uniformBuffer.get(), kUniformBlockSize),
1276 QRhiShaderResourceBinding::sampledTexture(1,
1277 QRhiShaderResourceBinding::FragmentStage,
1278 sliceSlot.texture.get(), k.sampler.get())});
1279 k.srb[slotIndex]->create();
1280 }
1281
1282 // Upload texture — R8 needs tightly-packed row data
1283 {
1284 const qsizetype tightStride = qsizetype(gray.width());
1285 QByteArray texData;
1286 texData.resize(gray.height() * tightStride);
1287 for (int y = 0; y < gray.height(); ++y) {
1288 memcpy(texData.data() + y * tightStride,
1289 gray.constScanLine(y),
1290 size_t(tightStride));
1291 }
1292 QRhiTextureSubresourceUploadDescription sub(texData.constData(), texData.size());
1293 sub.setSourceSize(imgSize);
1294 QRhiTextureUploadDescription desc({0, 0, sub});
1295 u->uploadTexture(sliceSlot.texture.get(), desc);
1296 }
1297
1298 // ── Vertex data ─────────────────────────────────────────────────
1299 slice->generateQuadVertices(sliceSlot.vertices);
1300 u->updateDynamicBuffer(k.vertexBuffer[slotIndex].get(), 0,
1301 static_cast<quint32>(sliceSlot.vertices.size() * sizeof(float)),
1302 sliceSlot.vertices.constData());
1303
1304 sliceSlot.opacity = slice->opacity();
1305 sliceSlot.windowCenter = slice->windowCenter();
1306 sliceSlot.windowWidth = slice->windowWidth();
1307 sliceSlot.visible = true;
1308}
1309
1310//=============================================================================================================
1311
1312int BrainRenderer::prepareSliceDraw(QRhiResourceUpdateBatch* u,
1313 const SceneData& data,
1314 int slotIndex)
1315{
1316 if (slotIndex < 0 || slotIndex >= Impl::kMaxSliceSlots)
1317 return -1;
1318
1319 auto& k = d->sliceRes;
1320 if (!k.initialized || !k.pipeline)
1321 return -1;
1322
1323 const auto& sliceSlot = k.sliceSlots[slotIndex];
1324 if (!sliceSlot.visible || d->insidePass("prepareSliceDraw"))
1325 return -1;
1326
1327 const int uniformOffset = k.currentUniformOffset;
1328 k.currentUniformOffset += k.uniformSlotStride;
1329 if (static_cast<quint32>(uniformOffset + kUniformBlockSize) > k.uniformBuffer->size())
1330 return -1;
1331
1332 // Uniform block matches slice.vert / slice.frag layout:
1333 // mat4 mvp (64 bytes)
1334 // mat4 sliceToWorld (64 bytes)
1335 // float opacity (4)
1336 // float windowCenter(4)
1337 // float windowWidth (4)
1338 // float _pad0 (4)
1339 struct
1340 {
1341 float mvp[16];
1342 float sliceToWorld[16];
1343 float opacity;
1344 float windowCenter;
1345 float windowWidth;
1346 float _pad0;
1347 } ub;
1348 memcpy(ub.mvp, data.mvp.constData(), 64);
1349 // SliceToWorld is identity for pre-transformed vertices (already in world coords)
1350 QMatrix4x4 identity;
1351 identity.setToIdentity();
1352 memcpy(ub.sliceToWorld, identity.constData(), 64);
1353 ub.opacity = sliceSlot.opacity;
1354 ub.windowCenter = sliceSlot.windowCenter;
1355 ub.windowWidth = sliceSlot.windowWidth;
1356 ub._pad0 = 0.0f;
1357 u->updateDynamicBuffer(k.uniformBuffer.get(), uniformOffset, sizeof(ub), &ub);
1358
1359 return uniformOffset;
1360}
1361
1362//=============================================================================================================
1363
1364void BrainRenderer::issueSliceDraw(QRhiCommandBuffer* cb,
1365 int slotIndex,
1366 int uniformOffset)
1367{
1368 if (slotIndex < 0 || slotIndex >= Impl::kMaxSliceSlots)
1369 return;
1370 if (uniformOffset < 0)
1371 return;
1372
1373 auto& k = d->sliceRes;
1374 if (!k.initialized || !k.pipeline)
1375 return;
1376
1377 cb->setGraphicsPipeline(k.pipeline.get());
1378 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
1379 cb->setShaderResources(k.srb[slotIndex].get(), 1, &srbOffset);
1380 const QRhiCommandBuffer::VertexInput vbuf(k.vertexBuffer[slotIndex].get(), 0);
1381 cb->setVertexInput(0, 1, &vbuf, k.indexBuffer.get(), 0, QRhiCommandBuffer::IndexUInt32);
1382 cb->drawIndexed(6);
1383}
1384
1385//=============================================================================================================
1386
1387int BrainRenderer::prepareSurfaceDraw(QRhiResourceUpdateBatch* u,
1388 const SceneData& data,
1389 BrainSurface* surface)
1390{
1391 if (!surface || !surface->isVisible() || d->insidePass("prepareSurfaceDraw"))
1392 return -1;
1393 if (!d->uploadSurface(surface, u))
1394 return -1;
1395
1396 const int offset = d->claimUniformSlot("prepareSurfaceDraw");
1397 if (offset < 0)
1398 return -1;
1399
1400 // A highlighted region or vertex range replaces the whole-surface glow
1401 const std::optional<uint32_t> label = surface->selectedLabel();
1402
1403 struct
1404 {
1405 float mvp[16]; // 0..63
1406 float cameraPos[3]; // 64..75
1407 float isSelected; // 76..79
1408 float lightDir[3]; // 80..91
1409 float tissueType; // 92..95
1410 float lightingEnabled; // 96..99
1411 float overlayMode; // 100..103
1412 float labelActive; // 104..107
1413 float pad; // 108..111
1414 float selectedLabel[4]; // 112..127
1415 } ub;
1416 memcpy(ub.mvp, data.mvp.constData(), 64);
1417 memcpy(ub.cameraPos, &data.cameraPos, 12);
1418 ub.isSelected = (surface->isSelected() && !label) ? 1.0f : 0.0f;
1419 memcpy(ub.lightDir, &data.lightDir, 12);
1420 ub.tissueType = static_cast<float>(surface->tissueType());
1421 ub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
1422 ub.overlayMode = data.overlayMode;
1423 ub.labelActive = label ? 1.0f : 0.0f;
1424 ub.pad = 0.0f;
1425 const uint32_t key = label.value_or(0u);
1426 for (int c = 0; c < 4; ++c)
1427 ub.selectedLabel[c] = float((key >> (8 * c)) & 0xFFu) / 255.0f;
1428
1429 memcpy(d->uniformShadow.data() + offset, &ub, sizeof(ub));
1430 return offset;
1431}
1432
1433//=============================================================================================================
1434
1435void BrainRenderer::issueSurfaceDraw(QRhiCommandBuffer* cb,
1436 BrainSurface* surface,
1437 ShaderMode mode,
1438 int uniformOffset)
1439{
1440 if (!surface || uniformOffset < 0)
1441 return;
1442
1443 auto* pipeline = d->pipelines[mode].get();
1444 const Impl::GpuMesh* m = d->findMesh(surface->renderKey());
1445 if (!pipeline || !m)
1446 return;
1447
1448 auto draw = [&](QRhiGraphicsPipeline* p) {
1449 cb->setGraphicsPipeline(p);
1450 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
1451 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
1452 d->bindSurface(cb, *m);
1453 cb->drawIndexed(surface->indexCount());
1454 };
1455
1456 if (mode == Holographic && d->pipelinesBackColor[Holographic]) {
1457 draw(d->pipelinesBackColor[Holographic].get());
1458 }
1459
1460 draw(pipeline);
1461}
1462
1463//=============================================================================================================
1464
1465int BrainRenderer::Impl::writeInstancedUniforms(const SceneData& data, const char* caller)
1466{
1467 const int offset = claimUniformSlot(caller);
1468 if (offset < 0)
1469 return -1;
1470
1471 struct
1472 {
1473 float mvp[16]; // 0..63
1474 float cameraPos[3]; // 64..75
1475 float _pad0; // 76..79
1476 float lightDir[3]; // 80..91
1477 float lightingEnabled; // 92..95: std140 packs a float into the vec3's last slot
1478 } ub;
1479 memcpy(ub.mvp, data.mvp.constData(), 64);
1480 memcpy(ub.cameraPos, &data.cameraPos, 12);
1481 ub._pad0 = 0.0f;
1482 memcpy(ub.lightDir, &data.lightDir, 12);
1483 ub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
1484 memcpy(uniformShadow.data() + offset, &ub, sizeof(ub));
1485 return offset;
1486}
1487
1488//=============================================================================================================
1489
1490int BrainRenderer::prepareDipoleDraw(QRhiResourceUpdateBatch* u, const SceneData& data, DipoleObject* dipoles)
1491{
1492 if (!dipoles || !dipoles->isVisible() || d->insidePass("prepareDipoleDraw"))
1493 return -1;
1494 if (!d->uploadInstanced(dipoles->renderKey(), dipoles->streams(), u))
1495 return -1;
1496 return d->writeInstancedUniforms(data, "prepareDipoleDraw");
1497}
1498
1499//=============================================================================================================
1500
1501int BrainRenderer::prepareNetworkDraw(QRhiResourceUpdateBatch* u, const SceneData& data, NetworkObject* network)
1502{
1503 if (!network || !network->isVisible() || !network->hasData() || d->insidePass("prepareNetworkDraw"))
1504 return -1;
1505 const bool nodes = d->uploadInstanced(network->nodeRenderKey(), network->nodeStreams(), u);
1506 const bool edges = d->uploadInstanced(network->edgeRenderKey(), network->edgeStreams(), u);
1507 if (!nodes && !edges)
1508 return -1;
1509 return d->writeInstancedUniforms(data, "prepareNetworkDraw");
1510}
1511
1512//=============================================================================================================
1513
1514int BrainRenderer::preparePolylineDraw(QRhiResourceUpdateBatch* u, const SceneData& data, PolylineObject* polyline)
1515{
1516 if (!polyline || !polyline->isVisible() || !polyline->hasData() || d->insidePass("preparePolylineDraw"))
1517 return -1;
1518 if (!d->uploadInstanced(polyline->renderKey(), polyline->streams(), u))
1519 return -1;
1520 return d->writeInstancedUniforms(data, "preparePolylineDraw");
1521}
1522
1523//=============================================================================================================
1524
1525int BrainRenderer::prepareElectrodeDraw(QRhiResourceUpdateBatch* u, const SceneData& data, ElectrodeObject* electrodes)
1526{
1527 if (!electrodes || !electrodes->isVisible() || d->insidePass("prepareElectrodeDraw"))
1528 return -1;
1529 const bool bodies = d->uploadInstanced(electrodes->bodyRenderKey(), electrodes->bodyStreams(), u);
1530 const bool contacts = d->uploadInstanced(electrodes->contactRenderKey(), electrodes->contactStreams(), u);
1531 if (!bodies && !contacts)
1532 return -1;
1533 return d->writeInstancedUniforms(data, "prepareElectrodeDraw");
1534}
1535
1536//=============================================================================================================
1537
1538void BrainRenderer::issueElectrodeDraw(QRhiCommandBuffer* cb, ElectrodeObject* electrodes, int uniformOffset)
1539{
1540 if (!electrodes || !electrodes->isVisible())
1541 return;
1542 QRhiGraphicsPipeline* pipeline = d->electrodePipeline.get();
1543 d->issueInstanced(cb, uniformOffset, electrodes->bodyRenderKey(), electrodes->bodyStreams(), pipeline);
1544 d->issueInstanced(cb, uniformOffset, electrodes->contactRenderKey(), electrodes->contactStreams(), pipeline);
1545}
1546
1547//=============================================================================================================
1548
1549void BrainRenderer::Impl::issueInstanced(QRhiCommandBuffer* cb,
1550 int uniformOffset,
1551 const RenderKey& key,
1552 const InstancedStreams& streams,
1553 QRhiGraphicsPipeline* pipeline)
1554{
1555 const GpuMesh* m = findMesh(key);
1556 if (!pipeline || !m || uniformOffset < 0 || streams.indexCount <= 0 || streams.instanceCount <= 0)
1557 return;
1558
1559 cb->setGraphicsPipeline(pipeline);
1560 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
1561 cb->setShaderResources(srb.get(), 1, &srbOffset);
1562 const QRhiCommandBuffer::VertexInput bindings[2] = {{m->buffers[0].get(), 0}, {m->buffers[1].get(), 0}};
1563 cb->setVertexInput(0, 2, bindings, m->buffers[2].get(), 0, QRhiCommandBuffer::IndexUInt32);
1564 cb->drawIndexed(quint32(streams.indexCount), quint32(streams.instanceCount));
1565}
1566
1567//=============================================================================================================
1568
1569void BrainRenderer::issueDipoleDraw(QRhiCommandBuffer* cb, DipoleObject* dipoles, int uniformOffset)
1570{
1571 if (dipoles && dipoles->isVisible())
1572 d->issueInstanced(cb, uniformOffset, dipoles->renderKey(), dipoles->streams(), d->pipelines[Dipole].get());
1573}
1574
1575//=============================================================================================================
1576
1577void BrainRenderer::issueNetworkDraw(QRhiCommandBuffer* cb, NetworkObject* network, int uniformOffset)
1578{
1579 if (!network || !network->isVisible() || !network->hasData())
1580 return;
1581 d->issueInstanced(cb, uniformOffset, network->nodeRenderKey(), network->nodeStreams(), d->pipelines[Dipole].get());
1582 d->issueInstanced(cb, uniformOffset, network->edgeRenderKey(), network->edgeStreams(), d->pipelines[Dipole].get());
1583}
1584
1585//=============================================================================================================
1586
1587void BrainRenderer::issuePolylineDraw(QRhiCommandBuffer* cb, PolylineObject* polyline, int uniformOffset)
1588{
1589 if (polyline && polyline->isVisible() && polyline->hasData())
1590 d->issueInstanced(cb, uniformOffset, polyline->renderKey(), polyline->streams(), d->pipelines[Dipole].get());
1591}
1592
1593} // namespace DISP3DLIB
Qt-RHI scene renderer: shader pipelines, lighting, dual render targets and per-frame draw orchestrati...
Instanced connectivity-graph renderable: node spheres and edge cylinders coloured by weight through a...
PolylineObject class declaration.
Instanced-arrow renderable for fitted equivalent current dipoles.
Renderable cortical / BEM mesh: CPU geometry, colour and annotation streams for the renderer.
Renderable view of an sEEG / ECoG implantation: contact spheres and lead bodies.
Single MRI volume slice rendered as a textured quad with adjustable axis, position,...
Generic live-RGB video texture overlay rendered as a screen-aligned quad with chroma keying.
3-D brain visualisation using the Qt RHI rendering backend.
CPU-side contents of one GPU buffer of a renderable.
Definition rendertypes.h:99
CPU streams of an instanced mesh: one shape (vertices, indices) drawn once per instance.
Identity of a renderable in the renderer's GPU resource cache.
std::weak_ptr< const void > token() const
const void * id() const
Geometry of one brain surface vertex: position and normal.
Renderable cortical surface mesh with per-vertex color, curvature data, and GPU buffer management.
GpuStream geometryStream() const
const RenderKey & renderKey() const
TissueType tissueType() const
GpuStream indexStream() const
uint32_t indexCount() const
std::optional< uint32_t > selectedLabel() const
GpuStream colorStream() const
GpuStream annotationStream() const
Renderable dipole arrow set with instanced GPU rendering.
const RenderKey & renderKey() const
InstancedStreams streams() const
Renderable sEEG / ECoG electrodes: contact spheres plus lead bodies.
const RenderKey & contactRenderKey() const
InstancedStreams bodyStreams() const
const RenderKey & bodyRenderKey() const
InstancedStreams contactStreams() const
Renderable connectivity network visualization.
InstancedStreams edgeStreams() const
const RenderKey & nodeRenderKey() const
InstancedStreams nodeStreams() const
const RenderKey & edgeRenderKey() const
Connected line segments through an ordered list of points.
const RenderKey & renderKey() const
InstancedStreams streams() const
Data model for a single 2-D MRI volume slice.
Definition sliceobject.h:82
float windowCenter() const
const QImage & image() const
float windowWidth() const
void generateQuadVertices(QVector< float > &vertices) const
Camera-facing textured quad rendered at a focus point in the 3-D scene.
QVector3D focusPosition() const
float sizeMeters() const
QVector3D upHint() const
Hint direction used as the quad's "up" axis. When set (non-zero), the quad's long edge is perpendicul...
const QImage & frame() const
quint64 frameGeneration() const
quint64 depthFrameGeneration() const
const QImage & depthFrame() const
bool isDepthEnabled() const
GpuMesh & mesh(const RenderKey &key)
std::unique_ptr< QRhiRenderBuffer > dsBuffer
bool uploadInstanced(const RenderKey &key, const InstancedStreams &streams, QRhiResourceUpdateBatch *u)
static constexpr int kMaxStreams
bool uploadSurface(BrainSurface *surface, QRhiResourceUpdateBatch *u)
std::unique_ptr< QRhiBuffer > uniformBuffer
std::unique_ptr< QRhiTextureRenderTarget > rtClear
void createResources(QRhi *rhi, QRhiRenderPassDescriptor *rp, int sampleCount)
int claimUniformSlot(const char *caller)
bool videoOverlayUsesDepth(const VideoOverlay *overlay) const
static constexpr int kMaxSliceSlots
std::unique_ptr< QRhiTextureRenderTarget > rtPreserve
std::unique_ptr< QRhiShaderResourceBindings > srb
const GpuMesh * findMesh(const RenderKey &key) const
void beginPass(QRhiCommandBuffer *cb, QRhiRenderTarget *rt, QRhiResourceUpdateBatch *updates)
int writeInstancedUniforms(const SceneData &data, const char *caller)
std::unique_ptr< QRhiRenderPassDescriptor > rpClear
QRhiBuffer * upload(GpuMesh &mesh, int slot, QRhiBuffer::UsageFlags usage, const GpuStream &stream, QRhiResourceUpdateBatch *u)
void flushUniforms(QRhiResourceUpdateBatch *u)
void issueInstanced(QRhiCommandBuffer *cb, int uniformOffset, const RenderKey &key, const InstancedStreams &streams, QRhiGraphicsPipeline *pipeline)
std::unique_ptr< QRhiRenderPassDescriptor > rpPreserve
VideoOverlayResources videoOverlay
std::unordered_map< const void *, GpuMesh > meshes
std::unique_ptr< QRhiGraphicsPipeline > electrodePipeline
void bindSurface(QRhiCommandBuffer *cb, const GpuMesh &mesh)
std::array< std::unique_ptr< QRhiGraphicsPipeline >, kNumShaderModes > pipelinesBackColor
bool insidePass(const char *caller) const
std::array< std::unique_ptr< QRhiGraphicsPipeline >, kNumShaderModes > pipelines
std::weak_ptr< const void > owner
std::array< quint64, kMaxStreams > generations
std::array< std::unique_ptr< QRhiBuffer >, kMaxStreams > buffers
std::unique_ptr< QRhiGraphicsPipeline > surfaceDepthPipeline
std::unique_ptr< QRhiGraphicsPipeline > pipeline
std::unique_ptr< QRhiShaderResourceBindings > srb
std::unique_ptr< QRhiGraphicsPipeline > surfacePipeline
std::unique_ptr< QRhiShaderResourceBindings > srbDepth
std::unique_ptr< QRhiTexture > texture
std::unique_ptr< QRhiGraphicsPipeline > pipeline
std::unique_ptr< QRhiBuffer > vertexBuffer[kMaxSliceSlots]
std::unique_ptr< QRhiShaderResourceBindings > srb[kMaxSliceSlots]
int prepareDipoleDraw(QRhiResourceUpdateBatch *u, const SceneData &data, DipoleObject *dipoles)
static constexpr ShaderMode Dipole
int preparePolylineDraw(QRhiResourceUpdateBatch *u, const SceneData &data, PolylineObject *polyline)
void issuePolylineDraw(QRhiCommandBuffer *cb, PolylineObject *polyline, int uniformOffset)
int prepareElectrodeDraw(QRhiResourceUpdateBatch *u, const SceneData &data, ElectrodeObject *electrodes)
void issueElectrodeDraw(QRhiCommandBuffer *cb, ElectrodeObject *electrodes, int uniformOffset)
void prepareSlice(QRhi *rhi, QRhiResourceUpdateBatch *u, DISP3DLIB::SliceObject *slice, int slotIndex)
void ensureRenderTargets(QRhi *rhi, QRhiTexture *colorTex, const QSize &pixelSize)
int prepareSliceDraw(QRhiResourceUpdateBatch *u, const SceneData &data, int slotIndex)
void issueSliceDraw(QRhiCommandBuffer *cb, int slotIndex, int uniformOffset)
void beginFrame(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *updates=nullptr)
static constexpr ShaderMode ShowNormals
QRhiRenderTarget * rtPreserve() const
void initialize(QRhi *rhi, QRhiRenderPassDescriptor *rp, int sampleCount)
void issueSurfaceDraw(QRhiCommandBuffer *cb, BrainSurface *surface, ShaderMode mode, int uniformOffset)
QRhiRenderTarget * rtClear() const
int prepareNetworkDraw(QRhiResourceUpdateBatch *u, const SceneData &data, NetworkObject *network)
static constexpr ShaderMode Standard
void endPass(QRhiCommandBuffer *cb)
void beginPreservingPass(QRhiCommandBuffer *cb, QRhiResourceUpdateBatch *updates=nullptr)
int prepareSurfaceDraw(QRhiResourceUpdateBatch *u, const SceneData &data, BrainSurface *surface)
static constexpr ShaderMode Holographic
static constexpr ShaderMode Anatomical
void issueNetworkDraw(QRhiCommandBuffer *cb, NetworkObject *network, int uniformOffset)
void issueDipoleDraw(QRhiCommandBuffer *cb, DipoleObject *dipoles, int uniformOffset)
void prepareVideoOverlay(QRhi *rhi, QRhiResourceUpdateBatch *u, DISP3DLIB::VideoOverlay *overlay)
static constexpr ShaderMode XRay
void issueVideoOverlayDraw(QRhiCommandBuffer *cb, DISP3DLIB::VideoOverlay *overlay, BrainSurface *surface, int uniformOffset)
DISP3DLIB::ShaderMode ShaderMode
int prepareVideoOverlayDraw(QRhiResourceUpdateBatch *u, const SceneData &data, DISP3DLIB::VideoOverlay *overlay, BrainSurface *surface)
Per-draw scene uniforms: camera matrices, light and overlay mode.