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>
26
28
29#include <QFile>
30#include <QDebug>
31#include <QImage>
32#include <QVector3D>
33#include <array>
34#include <limits>
35#include <map>
36#include <cstring>
37
38namespace DISP3DLIB
39{
40
41//=============================================================================================================
42// PIMPL
43//=============================================================================================================
44
45static constexpr int kNumShaderModes = 6; // Standard..ShowNormals
46
48{
49 void createResources(QRhi* rhi, QRhiRenderPassDescriptor* rp, int sampleCount);
50
51 // Offset of the next per-draw uniform slot, or -1 (with a warning) when the buffer is full
52 int claimUniformSlot(const char* caller);
53
54 std::unique_ptr<QRhiShaderResourceBindings> srb;
55
56 // Pipelines for each mode — indexed by ShaderMode enum (O(1) lookup)
57 std::array<std::unique_ptr<QRhiGraphicsPipeline>, kNumShaderModes> pipelines{};
58 std::array<std::unique_ptr<QRhiGraphicsPipeline>, kNumShaderModes> pipelinesBackColor{};
59
60 std::unique_ptr<QRhiBuffer> uniformBuffer;
63 bool overflowReported = false; // Reset per frame so the log gets one line, not one per skipped draw
64
65 bool resourcesDirty = true;
66
67 // ── Dual render targets for multi-pass rendering ─────────────────
68 // Qt bakes load/store flags at create() time, so we need two separate
69 // render targets sharing the same color texture + depth buffer:
70 // - rtClear: clears framebuffer (first pass of each frame)
71 // - rtPreserve: preserves contents (subsequent passes)
72 // Validated in test_wasm_multi_pass on both Metal and WebGL.
73 std::unique_ptr<QRhiRenderBuffer> dsBuffer;
74 std::unique_ptr<QRhiTextureRenderTarget> rtClear;
75 std::unique_ptr<QRhiTextureRenderTarget> rtPreserve;
76 std::unique_ptr<QRhiRenderPassDescriptor> rpClear;
77 std::unique_ptr<QRhiRenderPassDescriptor> rpPreserve;
78 QSize rtSize;
79 QRhiTexture* rtColorTex = nullptr; // Track texture pointer for rebuild
80
81 // ── WORKAROUND(QRhi-GLES2): merged single-drawIndexed buffers ────
82 // Used on WASM to avoid the multi-drawIndexed bug in QRhi's GLES2
83 // backend. Each surface category (brain, BEM, sensors, etc.) gets
84 // its own merged buffer set, drawn in separate render passes.
85 // Remove when upstream Qt fixes the issue.
87 {
88 QVector<BrainSurface*> surfaces;
89 std::unique_ptr<QRhiBuffer> vertexBuffer;
90 std::unique_ptr<QRhiBuffer> indexBuffer;
91 int indexCount = 0;
92 int totalVertexCount = 0; // cached vertex count from last full rebuild
93 bool dirty = true; // Geometry needs rebuild (surface list changed)
94 bool gpuVertexDirty = true; // Vertex data changed, needs GPU re-upload
95 bool gpuIndexDirty = true; // Index data changed, needs GPU re-upload
96 QByteArray vertexRaw;
97 QByteArray indexRaw;
98 QVector<quint64> surfaceGenerations; // per-surface vertex generation snapshot
99 };
100 std::map<QString, MergedGroup> mergedGroups; // keyed by category name
101
102 // ── Generic video overlay ───────────────────────────────────────
103 // Camera-facing textured quad rendered last (depth test off) at the
104 // current focus point. Resources are created lazily on first use.
106 {
107 std::unique_ptr<QRhiGraphicsPipeline> pipeline;
108 std::unique_ptr<QRhiGraphicsPipeline> surfacePipeline;
109 std::unique_ptr<QRhiGraphicsPipeline> surfaceDepthPipeline; // POM-enhanced decal
110 std::unique_ptr<QRhiShaderResourceBindings> srb;
111 std::unique_ptr<QRhiShaderResourceBindings> srbDepth; // SRB with depth texture at binding 2
112 std::unique_ptr<QRhiBuffer> uniformBuffer;
113 std::unique_ptr<QRhiBuffer> vertexBuffer;
114 std::unique_ptr<QRhiBuffer> indexBuffer;
115 std::unique_ptr<QRhiTexture> texture;
116 std::unique_ptr<QRhiTexture> depthTexture;
117 std::unique_ptr<QRhiSampler> sampler;
118 std::unique_ptr<QRhiSampler> depthSampler; // mipmap-enabled for vertex displacement
123 quint64 uploadedFrameGen = std::numeric_limits<quint64>::max();
124 quint64 uploadedDepthFrameGen = std::numeric_limits<quint64>::max();
125 bool indexUploaded = false;
126 bool initialized = false;
127 bool depthInitialized = false;
128 };
130
131 // ── MRI slice rendering ─────────────────────────────────────────
132 // Up to 3 ortho slices (axial, coronal, sagittal) rendered as
133 // textured quads with depth test and alpha blending.
134 static constexpr int kMaxSliceSlots = 3;
136 {
137 std::unique_ptr<QRhiTexture> texture;
139 bool dirty = true; // needs texture re-upload
140 bool visible = false; // whether this slot has valid data
141 QVector<float> vertices; // 4 verts × (3 pos + 2 uv) = 20 floats
142 float opacity = 0.8f;
143 float windowCenter = 0.5f;
144 float windowWidth = 1.0f;
145 };
147 {
148 std::unique_ptr<QRhiGraphicsPipeline> pipeline;
149 std::unique_ptr<QRhiShaderResourceBindings> srb[kMaxSliceSlots];
150 std::unique_ptr<QRhiBuffer> uniformBuffer;
151 std::unique_ptr<QRhiBuffer> vertexBuffer[kMaxSliceSlots];
152 std::unique_ptr<QRhiBuffer> indexBuffer;
153 std::unique_ptr<QRhiSampler> sampler;
157 bool indexUploaded = false;
158 bool initialized = false;
159 };
161};
162
163//=============================================================================================================
164// Helpers
165//=============================================================================================================
166
167static inline QRhiViewport toViewport(const BrainRenderer::SceneData& d)
168{
169 return QRhiViewport(d.viewportX, d.viewportY, d.viewportW, d.viewportH);
170}
171
172static inline QRhiScissor toScissor(const BrainRenderer::SceneData& d)
173{
174 return QRhiScissor(d.scissorX, d.scissorY, d.scissorW, d.scissorH);
175}
176
177static QImage tightlyPackedRgba(const QImage& source)
178{
179 if (source.isNull())
180 return {};
181
182 QImage rgba = source.convertToFormat(QImage::Format_RGBA8888);
183 const qsizetype tightStride = qsizetype(rgba.width()) * 4;
184 if (rgba.bytesPerLine() == tightStride)
185 return rgba.copy();
186
187 QImage packed(rgba.size(), QImage::Format_RGBA8888);
188 for (int y = 0; y < rgba.height(); ++y) {
189 memcpy(packed.scanLine(y), rgba.constScanLine(y), size_t(tightStride));
190 }
191 return packed;
192}
193
194//=============================================================================================================
195// Uniform buffer layout constants — single source of truth for shader ↔ C++ interface
196//=============================================================================================================
197
198namespace
199{
200// Uniform buffer sizing
201constexpr int kUniformSlotCount = 8192; // Max draw calls before overflow
202constexpr int kUniformBlockSize = 256; // Bound size per SRB dynamic slot (bytes)
203
204// Per-object uniform byte offsets (must match .vert shader layout).
205// Kept as documentation of the UBO layout even where not directly indexed.
206[[maybe_unused]] constexpr int kOffsetMVP = 0; // mat4 (64 bytes)
207[[maybe_unused]] constexpr int kOffsetCameraPos = 64; // vec3 (12 bytes)
208[[maybe_unused]] constexpr int kOffsetSelected = 76; // float
209[[maybe_unused]] constexpr int kOffsetLightDir = 80; // vec3 (12 bytes)
210[[maybe_unused]] constexpr int kOffsetTissueType = 92; // float
211[[maybe_unused]] constexpr int kOffsetLighting = 96; // float
212[[maybe_unused]] constexpr int kOffsetOverlayMode = 100; // float
213[[maybe_unused]] constexpr int kOffsetSelectedSurfaceId = 104; // float — WORKAROUND(QRhi-GLES2)
214}
215
216//=============================================================================================================
217// DEFINE MEMBER METHODS
218//=============================================================================================================
219
220//=============================================================================================================
221
223: d(std::make_unique<Impl>())
224{
225}
226
227//=============================================================================================================
228
230
231//=============================================================================================================
232
233void BrainRenderer::initialize(QRhi* rhi, QRhiRenderPassDescriptor* rp, int sampleCount)
234{
235 if (d->resourcesDirty) {
236 d->createResources(rhi, rp, sampleCount);
237 }
238}
239
240//=============================================================================================================
241
243{
244 const int offset = currentUniformOffset;
245 if (static_cast<quint32>(offset + uniformBufferOffsetAlignment) > uniformBuffer->size()) {
246 if (!overflowReported) {
247 qWarning("BrainRenderer: uniform buffer full at %s; further draws this frame are skipped", caller);
248 overflowReported = true;
249 }
250 return -1;
251 }
253 return offset;
254}
255
256//=============================================================================================================
257
258void BrainRenderer::Impl::createResources(QRhi* rhi, QRhiRenderPassDescriptor* rp, int sampleCount)
259{
260 uniformBufferOffsetAlignment = rhi->ubufAlignment();
261
262 // Create Uniform Buffer
263 if (!uniformBuffer) {
264 // Size for 8192 slots with alignment — enough for 4 viewports × ~1000 surfaces
265 uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, kUniformSlotCount * uniformBufferOffsetAlignment));
266 uniformBuffer->create();
267 }
268
269 // Create SRB
270 if (!srb) {
271 srb.reset(rhi->newShaderResourceBindings());
272 srb->setBindings({// Use dynamic offset for the uniform buffer.
273 // The size of one uniform block in the shader is ~104 bytes,
274 // but we use uniformBufferOffsetAlignment for the stride.
275 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, uniformBuffer.get(), kUniformBlockSize)});
276 srb->create();
277 }
278
279 // Shader Loader
280 auto getShader = [](const QString& name) {
281 QFile f(name);
282 return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
283 };
284
285 // List of modes to initialize
286 QList<ShaderMode> modes = {Standard, Holographic, Anatomical, Dipole, XRay, ShowNormals};
287
288 for (ShaderMode mode : modes) {
289 QString vert = (mode == Holographic || mode == XRay) ? ":/holographic.vert.qsb" : (mode == Anatomical) ? ":/anatomical.vert.qsb"
290 : (mode == Dipole) ? ":/dipole.vert.qsb"
291 : (mode == ShowNormals) ? ":/shownormals.vert.qsb"
292 : ":/standard.vert.qsb";
293
294 QString frag = (mode == Holographic || mode == XRay) ? ":/holographic.frag.qsb" : (mode == Anatomical) ? ":/anatomical.frag.qsb"
295 : (mode == Dipole) ? ":/dipole.frag.qsb"
296 : (mode == ShowNormals) ? ":/shownormals.frag.qsb"
297 : ":/standard.frag.qsb";
298
299 QShader vS = getShader(vert);
300 QShader fS = getShader(frag);
301
302 if (!vS.isValid() || !fS.isValid()) {
303 qWarning() << "BrainRenderer: Could not load shaders for mode" << mode << vert << frag;
304 continue;
305 }
306
307 // Setup Pipeline
308 auto pipeline = std::unique_ptr<QRhiGraphicsPipeline>(rhi->newGraphicsPipeline());
309
310 QRhiGraphicsPipeline::TargetBlend blend;
311 if (mode == Holographic || mode == XRay) {
312 blend.enable = true;
313 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
314 blend.dstColor = QRhiGraphicsPipeline::One;
315 blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha;
316 blend.dstAlpha = QRhiGraphicsPipeline::One;
317 } else if (mode == Dipole) {
318 blend.enable = true;
319 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
320 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
321 blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha;
322 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
323 }
324
325 auto setup = [&](QRhiGraphicsPipeline* p, QRhiGraphicsPipeline::CullMode cull) {
326 p->setShaderStages({{QRhiShaderStage::Vertex, vS}, {QRhiShaderStage::Fragment, fS}});
327
328 QRhiVertexInputLayout il;
329
330 if (mode == Dipole) {
331 il.setBindings({
332 {6 * sizeof(float)}, // Binding 0: Vertex Data (Pos + Normal) -> stride 6 floats
333 {21 * sizeof(float), QRhiVertexInputBinding::PerInstance} // Binding 1: Instance Data (Mat4 + Color + Selected) -> stride 21 floats
334 });
335
336 il.setAttributes({ // Vertex Buffer (Binding 0)
337 {0, 0, QRhiVertexInputAttribute::Float3, 0}, // Pos
338 {0, 1, QRhiVertexInputAttribute::Float3, 3 * sizeof(float)}, // Normal
339
340 // Instance Buffer (Binding 1)
341 // Model Matrix (4 x vec4)
342 {1, 2, QRhiVertexInputAttribute::Float4, 0},
343 {1, 3, QRhiVertexInputAttribute::Float4, 4 * sizeof(float)},
344 {1, 4, QRhiVertexInputAttribute::Float4, 8 * sizeof(float)},
345 {1, 5, QRhiVertexInputAttribute::Float4, 12 * sizeof(float)},
346 // Color
347 {1, 6, QRhiVertexInputAttribute::Float4, 16 * sizeof(float)},
348 // isSelected
349 {1, 7, QRhiVertexInputAttribute::Float, 20 * sizeof(float)}});
350 } else {
351 il.setBindings({{36}}); // sizeof(VertexData) = 36 with surfaceId
352 il.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
353 {0, 1, QRhiVertexInputAttribute::Float3, 12},
354 {0, 2, QRhiVertexInputAttribute::UNormByte4, 24},
355 {0, 3, QRhiVertexInputAttribute::UNormByte4, 28},
356 {0, 4, QRhiVertexInputAttribute::Float, 32}}); // surfaceId
357 }
358
359 p->setVertexInputLayout(il);
360 p->setShaderResourceBindings(srb.get());
361 p->setRenderPassDescriptor(rp);
362 p->setSampleCount(sampleCount);
363 p->setCullMode(cull);
364 if (mode == Holographic) {
365 p->setTargetBlends({blend});
366 p->setDepthTest(true);
367 p->setDepthWrite(false);
368 } else if (mode == XRay) {
369 p->setTargetBlends({blend});
370 p->setDepthTest(false); // Disable Depth Test to see through head
371 p->setDepthWrite(false);
372 } else if (mode == Dipole) {
373 p->setTargetBlends({blend});
374 p->setCullMode(QRhiGraphicsPipeline::None);
375 p->setDepthTest(true);
376 p->setDepthWrite(false);
377 } else {
378 p->setDepthTest(true);
379 p->setDepthWrite(true);
380 }
381 p->setFlags(QRhiGraphicsPipeline::UsesScissor);
382 p->create();
383 };
384
385 if (mode == Holographic || mode == XRay) { // Handle XRay back-faces same as Holographic
386 auto pipelineBack = std::unique_ptr<QRhiGraphicsPipeline>(rhi->newGraphicsPipeline());
387 setup(pipelineBack.get(), QRhiGraphicsPipeline::Front);
388 pipelinesBackColor[mode] = std::move(pipelineBack);
389 setup(pipeline.get(), QRhiGraphicsPipeline::Back); // Front faces
390 } else {
391 // Culling: None (Double-sided) to be safe for FreeSurfer meshes
392 setup(pipeline.get(), QRhiGraphicsPipeline::None);
393 }
394 pipelines[mode] = std::move(pipeline);
395 }
396
397 resourcesDirty = false;
398}
399
400//=============================================================================================================
401
402//=============================================================================================================
403
404void BrainRenderer::ensureRenderTargets(QRhi* rhi, QRhiTexture* colorTex, const QSize& pixelSize)
405{
406 // Rebuild when size changes OR when the backing texture changes
407 // (QRhiWidget may return a different colorTexture() each frame).
408 if (d->rtClear && d->rtSize == pixelSize && d->rtColorTex == colorTex)
409 return;
410
411 d->rtSize = pixelSize;
412 d->rtColorTex = colorTex;
413
414 // Shared depth-stencil buffer
415 d->dsBuffer.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize));
416 d->dsBuffer->create();
417
418 QRhiColorAttachment colorAtt(colorTex);
419 QRhiTextureRenderTargetDescription desc(colorAtt);
420 desc.setDepthStencilBuffer(d->dsBuffer.get());
421
422 // RT 1: Clearing (no preserve flags) — used for the first pass of each frame
423 d->rtClear.reset(rhi->newTextureRenderTarget(desc));
424 d->rpClear.reset(d->rtClear->newCompatibleRenderPassDescriptor());
425 d->rtClear->setRenderPassDescriptor(d->rpClear.get());
426 d->rtClear->create();
427
428 // RT 2: Preserving (load previous contents) — used for passes 2+
429 d->rtPreserve.reset(rhi->newTextureRenderTarget(desc,
430 QRhiTextureRenderTarget::PreserveColorContents | QRhiTextureRenderTarget::PreserveDepthStencilContents));
431 d->rpPreserve.reset(d->rtPreserve->newCompatibleRenderPassDescriptor());
432 d->rtPreserve->setRenderPassDescriptor(d->rpPreserve.get());
433 d->rtPreserve->create();
434}
435
436//=============================================================================================================
437
438QRhiRenderTarget* BrainRenderer::rtClear() const
439{
440 return d->rtClear.get();
441}
442
443QRhiRenderTarget* BrainRenderer::rtPreserve() const
444{
445 return d->rtPreserve.get();
446}
447
448//=============================================================================================================
449
450void BrainRenderer::beginFrame(QRhiCommandBuffer* cb)
451{
452 d->currentUniformOffset = 0;
453 d->overflowReported = false;
454 d->sliceRes.currentUniformOffset = 0;
455
456 auto* rt = d->rtClear.get();
457 cb->beginPass(rt, QColor(0, 0, 0), {1.0f, 0});
458 const int w = rt->pixelSize().width();
459 const int h = rt->pixelSize().height();
460 cb->setViewport(QRhiViewport(0, 0, w, h));
461 cb->setScissor(QRhiScissor(0, 0, w, h));
462}
463
464//=============================================================================================================
465
466void BrainRenderer::updateSceneUniforms([[maybe_unused]] QRhi* rhi, [[maybe_unused]] const SceneData& data)
467{
468 // NO-OP: packed into per-object slots for simplicity
469}
470
471//=============================================================================================================
472
473void BrainRenderer::beginPreservingPass(QRhiCommandBuffer* cb)
474{
475 auto* rt = d->rtPreserve.get();
476 cb->beginPass(rt, QColor(0, 0, 0), {1.0f, 0});
477 const int w = rt->pixelSize().width();
478 const int h = rt->pixelSize().height();
479 cb->setViewport(QRhiViewport(0, 0, w, h));
480 cb->setScissor(QRhiScissor(0, 0, w, h));
481}
482
483//=============================================================================================================
484
485void BrainRenderer::endPass(QRhiCommandBuffer* cb)
486{
487 cb->endPass();
488}
489
490//=============================================================================================================
491// Video overlay rendering
492//=============================================================================================================
493
495 QRhiResourceUpdateBatch* u,
496 VideoOverlay* overlay)
497{
498 if (!overlay || !overlay->isEnabled())
499 return;
500 if (!overlay->hasFrame())
501 return;
502 if (!rhi || !u)
503 return;
504
505 QImage frame = tightlyPackedRgba(overlay->frame());
506 if (frame.isNull())
507 return;
508
509 auto& k = d->videoOverlay;
510 k.currentUniformOffset = 0;
511
512 // ── Lazy resource creation ──────────────────────────────────────
513 if (!k.initialized) {
514 // Shaders
515 QFile vFile(QStringLiteral(":/video_overlay.vert.qsb"));
516 QFile fFile(QStringLiteral(":/video_overlay.frag.qsb"));
517 if (!vFile.open(QIODevice::ReadOnly) || !fFile.open(QIODevice::ReadOnly)) {
518 qWarning() << "BrainRenderer: failed to open video overlay shaders";
519 return;
520 }
521 QShader vShader = QShader::fromSerialized(vFile.readAll());
522 QShader fShader = QShader::fromSerialized(fFile.readAll());
523 if (!vShader.isValid() || !fShader.isValid()) {
524 qWarning() << "BrainRenderer: invalid video overlay shaders";
525 return;
526 }
527
528 // Uniform buffer (mat4 + vec4 + 4 floats = 96 bytes, pad to 256)
529 k.uniformBufferOffsetAlignment = rhi->ubufAlignment();
530 k.uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
531 QRhiBuffer::UniformBuffer,
532 64 * k.uniformBufferOffsetAlignment));
533 k.uniformBuffer->create();
534
535 // Vertex buffer: 4 vertices × (3 pos + 2 uv) floats — refilled each frame
536 constexpr int kVbSize = 4 * 5 * sizeof(float);
537 k.vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
538 QRhiBuffer::VertexBuffer, kVbSize));
539 k.vertexBuffer->create();
540
541 // Index buffer: 2 triangles, 6 indices — uploaded once
542 constexpr int kIbSize = 6 * sizeof(quint32);
543 k.indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable,
544 QRhiBuffer::IndexBuffer, kIbSize));
545 k.indexBuffer->create();
546
547 // Sampler (linear, clamp)
548 k.sampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear,
549 QRhiSampler::None,
550 QRhiSampler::ClampToEdge,
551 QRhiSampler::ClampToEdge));
552 k.sampler->create();
553
554 // Initial texture uses the first real frame size.
555 k.texture.reset(rhi->newTexture(QRhiTexture::RGBA8, frame.size()));
556 k.texture->create();
557 k.textureSize = frame.size();
558
559 // SRB binding: uniform @ 0, sampled image @ 1
560 k.srb.reset(rhi->newShaderResourceBindings());
561 k.srb->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
562 QRhiShaderResourceBinding::VertexStage |
563 QRhiShaderResourceBinding::FragmentStage,
564 k.uniformBuffer.get(), kUniformBlockSize),
565 QRhiShaderResourceBinding::sampledTexture(1,
566 QRhiShaderResourceBinding::FragmentStage,
567 k.texture.get(), k.sampler.get())});
568 k.srb->create();
569
570 // Pipeline (alpha-blended, depth-test off so it sits on top)
571 QRhiGraphicsPipeline::TargetBlend blend;
572 blend.enable = true;
573 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
574 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
575 blend.srcAlpha = QRhiGraphicsPipeline::One;
576 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
577
578 k.pipeline.reset(rhi->newGraphicsPipeline());
579 k.pipeline->setShaderStages({{QRhiShaderStage::Vertex, vShader},
580 {QRhiShaderStage::Fragment, fShader}});
581 QRhiVertexInputLayout il;
582 il.setBindings({{5 * sizeof(float)}});
583 il.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
584 {0, 1, QRhiVertexInputAttribute::Float2, 3 * sizeof(float)}});
585 k.pipeline->setVertexInputLayout(il);
586 k.pipeline->setShaderResourceBindings(k.srb.get());
587 k.pipeline->setRenderPassDescriptor(d->rtClear->renderPassDescriptor());
588 k.pipeline->setSampleCount(d->rtClear->sampleCount());
589 k.pipeline->setCullMode(QRhiGraphicsPipeline::None);
590 k.pipeline->setTargetBlends({blend});
591 k.pipeline->setDepthTest(false);
592 k.pipeline->setDepthWrite(false);
593 k.pipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
594 k.pipeline->create();
595
596 QFile dvFile(QStringLiteral(":/video_decal.vert.qsb"));
597 QFile dfFile(QStringLiteral(":/video_decal.frag.qsb"));
598 if (!dvFile.open(QIODevice::ReadOnly) || !dfFile.open(QIODevice::ReadOnly)) {
599 qWarning() << "BrainRenderer: failed to open video decal shaders";
600 return;
601 }
602 QShader dvShader = QShader::fromSerialized(dvFile.readAll());
603 QShader dfShader = QShader::fromSerialized(dfFile.readAll());
604 if (!dvShader.isValid() || !dfShader.isValid()) {
605 qWarning() << "BrainRenderer: invalid video decal shaders";
606 return;
607 }
608
609 QRhiGraphicsPipeline::TargetBlend decalBlend;
610 decalBlend.enable = true;
611 decalBlend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
612 decalBlend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
613 decalBlend.srcAlpha = QRhiGraphicsPipeline::One;
614 decalBlend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
615
616 k.surfacePipeline.reset(rhi->newGraphicsPipeline());
617 k.surfacePipeline->setShaderStages({{QRhiShaderStage::Vertex, dvShader},
618 {QRhiShaderStage::Fragment, dfShader}});
619 QRhiVertexInputLayout dil;
620 dil.setBindings({{36}});
621 dil.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
622 {0, 1, QRhiVertexInputAttribute::Float3, 12},
623 {0, 2, QRhiVertexInputAttribute::UNormByte4, 24},
624 {0, 3, QRhiVertexInputAttribute::UNormByte4, 28},
625 {0, 4, QRhiVertexInputAttribute::Float, 32}});
626 k.surfacePipeline->setVertexInputLayout(dil);
627 k.surfacePipeline->setShaderResourceBindings(k.srb.get());
628 k.surfacePipeline->setRenderPassDescriptor(d->rtClear->renderPassDescriptor());
629 k.surfacePipeline->setSampleCount(d->rtClear->sampleCount());
630 k.surfacePipeline->setCullMode(QRhiGraphicsPipeline::None);
631 k.surfacePipeline->setTargetBlends({decalBlend});
632 k.surfacePipeline->setDepthTest(true);
633 k.surfacePipeline->setDepthWrite(false);
634 k.surfacePipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
635 k.surfacePipeline->create();
636
637 k.initialized = true;
638 }
639
640 // ── Lazy depth-enhanced pipeline creation ───────────────────────
641 if (overlay->isDepthEnabled() && !k.depthInitialized && k.initialized) {
642 QFile ddvFile(QStringLiteral(":/video_decal_depth.vert.qsb"));
643 QFile ddfFile(QStringLiteral(":/video_decal_depth.frag.qsb"));
644 if (ddvFile.open(QIODevice::ReadOnly) && ddfFile.open(QIODevice::ReadOnly)) {
645 QShader ddvShader = QShader::fromSerialized(ddvFile.readAll());
646 QShader ddfShader = QShader::fromSerialized(ddfFile.readAll());
647 if (ddvShader.isValid() && ddfShader.isValid()) {
648 // Mipmap-enabled sampler for depth texture (vertex shader
649 // samples a high LOD for smooth displacement on sparse meshes)
650 k.depthSampler.reset(rhi->newSampler(
651 QRhiSampler::Linear, QRhiSampler::Linear,
652 QRhiSampler::Linear, // mip filtering
653 QRhiSampler::ClampToEdge,
654 QRhiSampler::ClampToEdge));
655 k.depthSampler->create();
656
657 // 1x1 placeholder depth texture with mipmaps
658 k.depthTexture.reset(rhi->newTexture(
659 QRhiTexture::RGBA8, QSize(1, 1), 1, QRhiTexture::MipMapped));
660 k.depthTexture->create();
661 k.depthTextureSize = QSize(1, 1);
662
663 // SRB with depth texture at binding 2 (vertex + fragment)
664 k.srbDepth.reset(rhi->newShaderResourceBindings());
665 k.srbDepth->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
666 QRhiShaderResourceBinding::VertexStage |
667 QRhiShaderResourceBinding::FragmentStage,
668 k.uniformBuffer.get(), kUniformBlockSize),
669 QRhiShaderResourceBinding::sampledTexture(1,
670 QRhiShaderResourceBinding::FragmentStage,
671 k.texture.get(), k.sampler.get()),
672 QRhiShaderResourceBinding::sampledTexture(2,
673 QRhiShaderResourceBinding::VertexStage |
674 QRhiShaderResourceBinding::FragmentStage,
675 k.depthTexture.get(), k.depthSampler.get())});
676 k.srbDepth->create();
677
678 QRhiGraphicsPipeline::TargetBlend depthDecalBlend;
679 depthDecalBlend.enable = true;
680 depthDecalBlend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
681 depthDecalBlend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
682 depthDecalBlend.srcAlpha = QRhiGraphicsPipeline::One;
683 depthDecalBlend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
684
685 k.surfaceDepthPipeline.reset(rhi->newGraphicsPipeline());
686 k.surfaceDepthPipeline->setShaderStages({{QRhiShaderStage::Vertex, ddvShader},
687 {QRhiShaderStage::Fragment, ddfShader}});
688 QRhiVertexInputLayout ddil;
689 ddil.setBindings({{36}});
690 ddil.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
691 {0, 1, QRhiVertexInputAttribute::Float3, 12},
692 {0, 2, QRhiVertexInputAttribute::UNormByte4, 24},
693 {0, 3, QRhiVertexInputAttribute::UNormByte4, 28},
694 {0, 4, QRhiVertexInputAttribute::Float, 32}});
695 k.surfaceDepthPipeline->setVertexInputLayout(ddil);
696 k.surfaceDepthPipeline->setShaderResourceBindings(k.srbDepth.get());
697 k.surfaceDepthPipeline->setRenderPassDescriptor(d->rtClear->renderPassDescriptor());
698 k.surfaceDepthPipeline->setSampleCount(d->rtClear->sampleCount());
699 k.surfaceDepthPipeline->setCullMode(QRhiGraphicsPipeline::None);
700 k.surfaceDepthPipeline->setTargetBlends({depthDecalBlend});
701 k.surfaceDepthPipeline->setDepthTest(false); // inward-displaced vertices must not be culled by brain surface z
702 k.surfaceDepthPipeline->setDepthWrite(false);
703 k.surfaceDepthPipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
704 k.surfaceDepthPipeline->create();
705 k.depthInitialized = true;
706 }
707 }
708 }
709
710 // ── Index buffer (one-shot upload) ──────────────────────────────
711 if (!k.indexUploaded) {
712 const quint32 idx[6] = {0, 1, 2, 2, 1, 3};
713 u->uploadStaticBuffer(k.indexBuffer.get(), idx);
714 k.indexUploaded = true;
715 }
716
717 // ── Texture (re-create on size change, re-upload on new frame) ──
718 if (frame.size() != k.textureSize) {
719 k.texture.reset(rhi->newTexture(QRhiTexture::RGBA8, frame.size()));
720 k.texture->create();
721 k.textureSize = frame.size();
722 // Rebuild SRB to point at the new texture
723 k.srb->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
724 QRhiShaderResourceBinding::VertexStage |
725 QRhiShaderResourceBinding::FragmentStage,
726 k.uniformBuffer.get(), kUniformBlockSize),
727 QRhiShaderResourceBinding::sampledTexture(1,
728 QRhiShaderResourceBinding::FragmentStage,
729 k.texture.get(), k.sampler.get())});
730 k.srb->create();
731 // Also rebuild depth SRB if it exists
732 if (k.srbDepth && k.depthTexture) {
733 k.srbDepth->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
734 QRhiShaderResourceBinding::VertexStage |
735 QRhiShaderResourceBinding::FragmentStage,
736 k.uniformBuffer.get(), kUniformBlockSize),
737 QRhiShaderResourceBinding::sampledTexture(1,
738 QRhiShaderResourceBinding::FragmentStage,
739 k.texture.get(), k.sampler.get()),
740 QRhiShaderResourceBinding::sampledTexture(2,
741 QRhiShaderResourceBinding::VertexStage |
742 QRhiShaderResourceBinding::FragmentStage,
743 k.depthTexture.get(), k.depthSampler.get())});
744 k.srbDepth->create();
745 }
746 k.uploadedFrameGen = std::numeric_limits<quint64>::max();
747 }
748 if (overlay->frameGeneration() != k.uploadedFrameGen) {
749 QRhiTextureSubresourceUploadDescription sub(frame);
750 QRhiTextureUploadDescription desc({0, 0, sub});
751 u->uploadTexture(k.texture.get(), desc);
752 k.uploadedFrameGen = overlay->frameGeneration();
753 }
754
755 // ── Depth texture upload ────────────────────────────────────────
756 if (overlay->isDepthEnabled() && overlay->hasDepthFrame() && k.depthInitialized) {
757 QImage depthFrame = tightlyPackedRgba(overlay->depthFrame());
758 if (!depthFrame.isNull()) {
759 if (depthFrame.size() != k.depthTextureSize) {
760 k.depthTexture.reset(rhi->newTexture(
761 QRhiTexture::RGBA8, depthFrame.size(), 1, QRhiTexture::MipMapped));
762 k.depthTexture->create();
763 k.depthTextureSize = depthFrame.size();
764 // Rebuild depth SRB with new depth texture
765 k.srbDepth->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
766 QRhiShaderResourceBinding::VertexStage |
767 QRhiShaderResourceBinding::FragmentStage,
768 k.uniformBuffer.get(), kUniformBlockSize),
769 QRhiShaderResourceBinding::sampledTexture(1,
770 QRhiShaderResourceBinding::FragmentStage,
771 k.texture.get(), k.sampler.get()),
772 QRhiShaderResourceBinding::sampledTexture(2,
773 QRhiShaderResourceBinding::VertexStage |
774 QRhiShaderResourceBinding::FragmentStage,
775 k.depthTexture.get(), k.depthSampler.get())});
776 k.srbDepth->create();
777 k.uploadedDepthFrameGen = std::numeric_limits<quint64>::max();
778 }
779 if (overlay->depthFrameGeneration() != k.uploadedDepthFrameGen) {
780 QRhiTextureSubresourceUploadDescription depthSub(depthFrame);
781 QRhiTextureUploadDescription depthDesc({0, 0, depthSub});
782 u->uploadTexture(k.depthTexture.get(), depthDesc);
783 u->generateMips(k.depthTexture.get());
784 k.uploadedDepthFrameGen = overlay->depthFrameGeneration();
785 }
786 }
787 }
788}
789
790void BrainRenderer::renderVideoOverlay(QRhiCommandBuffer* cb, QRhi* rhi,
791 const SceneData& data,
792 VideoOverlay* overlay)
793{
794 if (!overlay || !overlay->isEnabled())
795 return;
796 if (!overlay->hasFrame())
797 return;
798
799 auto& k = d->videoOverlay;
800 if (!k.initialized)
801 return;
802 if (k.uniformBufferOffsetAlignment <= 0)
803 return;
804
805 const int uniformOffset = k.currentUniformOffset;
806 k.currentUniformOffset += k.uniformBufferOffsetAlignment;
807 if (static_cast<quint32>(uniformOffset + kUniformBlockSize) > k.uniformBuffer->size())
808 return;
809 QRhiResourceUpdateBatch* u = rhi->nextResourceUpdateBatch();
810
811 // ── Billboard the quad to face the camera ──────────────────────
812 const QVector3D centre = overlay->focusPosition();
813 const float sizeM = overlay->sizeMeters();
814
815 // Preserve the video frame's aspect ratio: sizeMeters controls the
816 // width, and the height is derived from the frame dimensions.
817 const QImage& frm = overlay->frame();
818 const float aspect = (frm.height() > 0)
819 ? static_cast<float>(frm.width()) / frm.height()
820 : 1.0f;
821 const float halfW = 0.5f * sizeM;
822 const float halfH = (aspect > 0.0f) ? halfW / aspect : halfW;
823
824 QVector3D viewDir = centre - data.cameraPos;
825 if (viewDir.lengthSquared() < 1e-12f)
826 viewDir = QVector3D(0, 0, -1);
827 viewDir.normalize();
828
829 // Use the upHint (tracker→objective axis) when available so that
830 // the quad's long edge is perpendicular to the optical axis.
831 const QVector3D hint = overlay->upHint();
832 QVector3D up;
833 if (hint.lengthSquared() > 1e-8f) {
834 // Project the hint onto the plane perpendicular to viewDir
835 up = (hint - QVector3D::dotProduct(hint, viewDir) * viewDir).normalized();
836 if (up.lengthSquared() < 1e-8f)
837 up = QVector3D(0.0f, 0.0f, 1.0f);
838 } else {
839 QVector3D worldUp(0.0f, 0.0f, 1.0f);
840 if (std::abs(QVector3D::dotProduct(viewDir, worldUp)) > 0.95f)
841 worldUp = QVector3D(0.0f, 1.0f, 0.0f);
842 up = QVector3D::crossProduct(
843 QVector3D::crossProduct(viewDir, worldUp), viewDir)
844 .normalized();
845 }
846 QVector3D right = QVector3D::crossProduct(viewDir, up).normalized();
847
848 const QVector3D c00 = centre - right * halfW - up * halfH;
849 const QVector3D c10 = centre + right * halfW - up * halfH;
850 const QVector3D c01 = centre - right * halfW + up * halfH;
851 const QVector3D c11 = centre + right * halfW + up * halfH;
852
853 const float verts[4 * 5] = {
854 c00.x(), c00.y(), c00.z(), 0.0f, 1.0f, // image y is flipped vs uv
855 c10.x(), c10.y(), c10.z(), 1.0f, 1.0f,
856 c01.x(), c01.y(), c01.z(), 0.0f, 0.0f,
857 c11.x(), c11.y(), c11.z(), 1.0f, 0.0f};
858 u->updateDynamicBuffer(k.vertexBuffer.get(), 0, sizeof(verts), verts);
859
860 // ── Uniforms (mat4 mvp, vec4 borderColor, float opacity, 3 pad) ─
861 struct
862 {
863 float mvp[16];
864 float borderColor[4];
865 float opacity;
866 float pad0, pad1, pad2;
867 } ub;
868 memcpy(ub.mvp, data.mvp.constData(), 64);
869 ub.borderColor[0] = 0.0f;
870 ub.borderColor[1] = 0.85f;
871 ub.borderColor[2] = 1.0f;
872 ub.borderColor[3] = 1.0f;
873 ub.opacity = overlay->opacity();
874 ub.pad0 = ub.pad1 = ub.pad2 = 0.0f;
875 u->updateDynamicBuffer(k.uniformBuffer.get(), uniformOffset, sizeof(ub), &ub);
876
877 cb->resourceUpdate(u);
878
879 // ── Draw ────────────────────────────────────────────────────────
880 cb->setViewport(toViewport(data));
881 cb->setScissor(toScissor(data));
882 cb->setGraphicsPipeline(k.pipeline.get());
883 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
884 cb->setShaderResources(k.srb.get(), 1, &srbOffset);
885 const QRhiCommandBuffer::VertexInput vbuf(k.vertexBuffer.get(), 0);
886 cb->setVertexInput(0, 1, &vbuf, k.indexBuffer.get(), 0, QRhiCommandBuffer::IndexUInt32);
887 cb->drawIndexed(6);
888}
889
890void BrainRenderer::renderVideoOverlayOnSurface(QRhiCommandBuffer* cb, QRhi* rhi,
891 const SceneData& data,
892 VideoOverlay* overlay,
893 BrainSurface* surface)
894{
895 Q_UNUSED(rhi);
896 if (!overlay || !overlay->isEnabled() || !overlay->hasFrame())
897 return;
898 if (!surface)
899 return;
900
901 auto& k = d->videoOverlay;
902 if (!k.initialized || !k.surfacePipeline)
903 return;
904 if (k.uniformBufferOffsetAlignment <= 0)
905 return;
906
907 const int uniformOffset = k.currentUniformOffset;
908 k.currentUniformOffset += k.uniformBufferOffsetAlignment;
909 if (static_cast<quint32>(uniformOffset + kUniformBlockSize) > k.uniformBuffer->size())
910 return;
911 QRhiResourceUpdateBatch* u = rhi->nextResourceUpdateBatch();
912
913 // Approximate the local scalp normal from the focus position vector.
914 QVector3D localNormal = overlay->focusPosition();
915 if (localNormal.lengthSquared() < 1e-12f) {
916 localNormal = QVector3D(0.0f, 0.0f, 1.0f);
917 }
918 localNormal.normalize();
919
920 // Use the tracker-derived up hint when available — this keeps the
921 // decal orientation locked to the physical microscope and avoids
922 // the 180° flip singularity that occurs with a fixed reference axis.
923 QVector3D referenceUp = overlay->upHint();
924 if (referenceUp.lengthSquared() < 1e-6f) {
925 // Fallback when no tracker up is available.
926 referenceUp = QVector3D(0.0f, 0.0f, 1.0f);
927 if (std::abs(QVector3D::dotProduct(localNormal, referenceUp)) > 0.95f) {
928 referenceUp = QVector3D(1.0f, 0.0f, 0.0f);
929 }
930 }
931 // Orthogonalise against the surface normal so the frame is tangent.
932 referenceUp = (referenceUp - QVector3D::dotProduct(referenceUp, localNormal) * localNormal);
933 if (referenceUp.lengthSquared() < 1e-12f) {
934 referenceUp = QVector3D(0.0f, 0.0f, 1.0f);
935 }
936 referenceUp.normalize();
937
938 const QVector3D axisV = referenceUp;
939 const QVector3D axisU = QVector3D::crossProduct(axisV, localNormal).normalized();
940
941 const bool useDepth = overlay->isDepthEnabled() && overlay->hasDepthFrame() && k.depthInitialized && k.surfaceDepthPipeline;
942
943 // Uniform block — the depth-enhanced shader has an extra vec4 depthParams
944 // but the base 7×vec4 layout (112 bytes) still fits within kUniformBlockSize
945 // (256 bytes) even with the extra vec4 (128 bytes total).
946 struct
947 {
948 float mvp[16];
949 float focusAndSize[4];
950 float axisUAndOpacity[4];
951 float axisVAndOffset[4];
952 float axisNAndDepth[4];
953 float cameraPosAndFacing[4];
954 float borderColor[4];
955 float depthParams[4];
956 } ub;
957 memcpy(ub.mvp, data.mvp.constData(), 64);
958 ub.focusAndSize[0] = overlay->focusPosition().x();
959 ub.focusAndSize[1] = overlay->focusPosition().y();
960 ub.focusAndSize[2] = overlay->focusPosition().z();
961 ub.focusAndSize[3] = overlay->sizeMeters();
962 ub.axisUAndOpacity[0] = axisU.x();
963 ub.axisUAndOpacity[1] = axisU.y();
964 ub.axisUAndOpacity[2] = axisU.z();
965 ub.axisUAndOpacity[3] = overlay->opacity();
966 ub.axisVAndOffset[0] = axisV.x();
967 ub.axisVAndOffset[1] = axisV.y();
968 ub.axisVAndOffset[2] = axisV.z();
969 ub.axisVAndOffset[3] = 0.00045f;
970 ub.axisNAndDepth[0] = localNormal.x();
971 ub.axisNAndDepth[1] = localNormal.y();
972 ub.axisNAndDepth[2] = localNormal.z();
973 ub.axisNAndDepth[3] = std::max(0.015f, overlay->sizeMeters() * 0.45f);
974 ub.cameraPosAndFacing[0] = data.cameraPos.x();
975 ub.cameraPosAndFacing[1] = data.cameraPos.y();
976 ub.cameraPosAndFacing[2] = data.cameraPos.z();
977 const QImage& decalFrame = overlay->frame();
978 ub.cameraPosAndFacing[3] = (decalFrame.height() > 0)
979 ? static_cast<float>(decalFrame.width()) / decalFrame.height()
980 : 1.0f;
981 ub.borderColor[0] = 0.0f;
982 ub.borderColor[1] = 0.85f;
983 ub.borderColor[2] = 1.0f;
984 ub.borderColor[3] = 1.0f;
985 ub.depthParams[0] = useDepth ? overlay->depthScale() : 0.0f;
986 ub.depthParams[1] = useDepth ? static_cast<float>(overlay->depthSteps()) : 0.0f;
987 ub.depthParams[2] = useDepth ? 1.0f : 0.0f;
988 ub.depthParams[3] = 0.0f;
989 u->updateDynamicBuffer(k.uniformBuffer.get(), uniformOffset, sizeof(ub), &ub);
990 cb->resourceUpdate(u);
991
992 cb->setViewport(toViewport(data));
993 cb->setScissor(toScissor(data));
994 if (useDepth) {
995 cb->setGraphicsPipeline(k.surfaceDepthPipeline.get());
996 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
997 cb->setShaderResources(k.srbDepth.get(), 1, &srbOffset);
998 } else {
999 cb->setGraphicsPipeline(k.surfacePipeline.get());
1000 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
1001 cb->setShaderResources(k.srb.get(), 1, &srbOffset);
1002 }
1003 const QRhiCommandBuffer::VertexInput vbuf(surface->vertexBuffer(), 0);
1004 cb->setVertexInput(0, 1, &vbuf, surface->indexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
1005 cb->drawIndexed(surface->indexCount());
1006}
1007
1008//=============================================================================================================
1009// MRI slice rendering
1010//=============================================================================================================
1011
1013 QRhiResourceUpdateBatch* u,
1015 int slotIndex)
1016{
1017 if (!rhi || !u)
1018 return;
1019 if (slotIndex < 0 || slotIndex >= Impl::kMaxSliceSlots)
1020 return;
1021
1022 auto& k = d->sliceRes;
1023 auto& sliceSlot = k.sliceSlots[slotIndex];
1024
1025 if (!slice) {
1026 sliceSlot.visible = false;
1027 return;
1028 }
1029
1030 const QImage& img = slice->image();
1031 if (img.isNull()) {
1032 sliceSlot.visible = false;
1033 return;
1034 }
1035
1036 // ── Lazy one-time resource creation ─────────────────────────────
1037 if (!k.initialized) {
1038 QFile vFile(QStringLiteral(":/slice.vert.qsb"));
1039 QFile fFile(QStringLiteral(":/slice.frag.qsb"));
1040 if (!vFile.open(QIODevice::ReadOnly) || !fFile.open(QIODevice::ReadOnly)) {
1041 qWarning() << "BrainRenderer: failed to open MRI slice shaders";
1042 return;
1043 }
1044 QShader vShader = QShader::fromSerialized(vFile.readAll());
1045 QShader fShader = QShader::fromSerialized(fFile.readAll());
1046 if (!vShader.isValid() || !fShader.isValid()) {
1047 qWarning() << "BrainRenderer: invalid MRI slice shaders";
1048 return;
1049 }
1050
1051 k.uniformBufferOffsetAlignment = rhi->ubufAlignment();
1052 k.uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1053 QRhiBuffer::UniformBuffer,
1054 64 * k.uniformBufferOffsetAlignment));
1055 k.uniformBuffer->create();
1056
1057 // Index buffer (shared by all 3 slots): 2 triangles = 6 indices
1058 k.indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Immutable,
1059 QRhiBuffer::IndexBuffer,
1060 6 * sizeof(quint32)));
1061 k.indexBuffer->create();
1062
1063 k.sampler.reset(rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear,
1064 QRhiSampler::None,
1065 QRhiSampler::ClampToEdge,
1066 QRhiSampler::ClampToEdge));
1067 k.sampler->create();
1068
1069 // Per-slot resources: vertex buffer + texture + SRB
1070 for (int i = 0; i < Impl::kMaxSliceSlots; ++i) {
1071 constexpr int kVbSize = 4 * 5 * sizeof(float); // 4 verts × (3 pos + 2 uv)
1072 k.vertexBuffer[i].reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1073 QRhiBuffer::VertexBuffer, kVbSize));
1074 k.vertexBuffer[i]->create();
1075
1076 // 1×1 placeholder texture — real size set on first data
1077 k.sliceSlots[i].texture.reset(rhi->newTexture(QRhiTexture::R8, QSize(1, 1)));
1078 k.sliceSlots[i].texture->create();
1079 k.sliceSlots[i].textureSize = QSize(1, 1);
1080
1081 k.srb[i].reset(rhi->newShaderResourceBindings());
1082 k.srb[i]->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
1083 QRhiShaderResourceBinding::VertexStage |
1084 QRhiShaderResourceBinding::FragmentStage,
1085 k.uniformBuffer.get(), kUniformBlockSize),
1086 QRhiShaderResourceBinding::sampledTexture(1,
1087 QRhiShaderResourceBinding::FragmentStage,
1088 k.sliceSlots[i].texture.get(), k.sampler.get())});
1089 k.srb[i]->create();
1090 }
1091
1092 // Pipeline: alpha-blended, depth-test on, depth-write off
1093 QRhiGraphicsPipeline::TargetBlend blend;
1094 blend.enable = true;
1095 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
1096 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1097 blend.srcAlpha = QRhiGraphicsPipeline::One;
1098 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
1099
1100 k.pipeline.reset(rhi->newGraphicsPipeline());
1101 k.pipeline->setShaderStages({{QRhiShaderStage::Vertex, vShader},
1102 {QRhiShaderStage::Fragment, fShader}});
1103 QRhiVertexInputLayout il;
1104 il.setBindings({{5 * sizeof(float)}});
1105 il.setAttributes({{0, 0, QRhiVertexInputAttribute::Float3, 0},
1106 {0, 1, QRhiVertexInputAttribute::Float2, 3 * sizeof(float)}});
1107 k.pipeline->setVertexInputLayout(il);
1108 k.pipeline->setShaderResourceBindings(k.srb[0].get());
1109 k.pipeline->setRenderPassDescriptor(d->rtClear->renderPassDescriptor());
1110 k.pipeline->setSampleCount(d->rtClear->sampleCount());
1111 k.pipeline->setCullMode(QRhiGraphicsPipeline::None);
1112 k.pipeline->setTargetBlends({blend});
1113 k.pipeline->setDepthTest(true);
1114 k.pipeline->setDepthWrite(false);
1115 k.pipeline->setFlags(QRhiGraphicsPipeline::UsesScissor);
1116 k.pipeline->create();
1117
1118 k.initialized = true;
1119 }
1120
1121 // ── Index buffer (one-shot upload) ──────────────────────────────
1122 if (!k.indexUploaded) {
1123 const quint32 idx[6] = {0, 1, 2, 2, 1, 3};
1124 u->uploadStaticBuffer(k.indexBuffer.get(), idx);
1125 k.indexUploaded = true;
1126 }
1127
1128 // ── Texture (re-create on size change, upload data) ─────────────
1129 // Convert to R8 (grayscale) for the slice shader
1130 QImage gray = img.convertToFormat(QImage::Format_Grayscale8);
1131 const QSize imgSize = gray.size();
1132
1133 if (imgSize != sliceSlot.textureSize) {
1134 sliceSlot.texture.reset(rhi->newTexture(QRhiTexture::R8, imgSize));
1135 sliceSlot.texture->create();
1136 sliceSlot.textureSize = imgSize;
1137 // Rebuild SRB to point at new texture
1138 k.srb[slotIndex]->setBindings({QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0,
1139 QRhiShaderResourceBinding::VertexStage |
1140 QRhiShaderResourceBinding::FragmentStage,
1141 k.uniformBuffer.get(), kUniformBlockSize),
1142 QRhiShaderResourceBinding::sampledTexture(1,
1143 QRhiShaderResourceBinding::FragmentStage,
1144 sliceSlot.texture.get(), k.sampler.get())});
1145 k.srb[slotIndex]->create();
1146 }
1147
1148 // Upload texture — R8 needs tightly-packed row data
1149 {
1150 const qsizetype tightStride = qsizetype(gray.width());
1151 QByteArray texData;
1152 texData.resize(gray.height() * tightStride);
1153 for (int y = 0; y < gray.height(); ++y) {
1154 memcpy(texData.data() + y * tightStride,
1155 gray.constScanLine(y),
1156 size_t(tightStride));
1157 }
1158 QRhiTextureSubresourceUploadDescription sub(texData.constData(), texData.size());
1159 sub.setSourceSize(imgSize);
1160 QRhiTextureUploadDescription desc({0, 0, sub});
1161 u->uploadTexture(sliceSlot.texture.get(), desc);
1162 }
1163
1164 // ── Vertex data ─────────────────────────────────────────────────
1165 slice->generateQuadVertices(sliceSlot.vertices);
1166 u->updateDynamicBuffer(k.vertexBuffer[slotIndex].get(), 0,
1167 static_cast<quint32>(sliceSlot.vertices.size() * sizeof(float)),
1168 sliceSlot.vertices.constData());
1169
1170 sliceSlot.opacity = slice->opacity();
1171 sliceSlot.windowCenter = slice->windowCenter();
1172 sliceSlot.windowWidth = slice->windowWidth();
1173 sliceSlot.visible = true;
1174}
1175
1176//=============================================================================================================
1177
1178int BrainRenderer::prepareSliceDraw(QRhiResourceUpdateBatch* u,
1179 const SceneData& data,
1180 int slotIndex)
1181{
1182 if (slotIndex < 0 || slotIndex >= Impl::kMaxSliceSlots)
1183 return -1;
1184
1185 auto& k = d->sliceRes;
1186 if (!k.initialized || !k.pipeline)
1187 return -1;
1188
1189 const auto& sliceSlot = k.sliceSlots[slotIndex];
1190 if (!sliceSlot.visible)
1191 return -1;
1192 if (k.uniformBufferOffsetAlignment <= 0)
1193 return -1;
1194
1195 const int uniformOffset = k.currentUniformOffset;
1196 k.currentUniformOffset += k.uniformBufferOffsetAlignment;
1197 if (static_cast<quint32>(uniformOffset + kUniformBlockSize) > k.uniformBuffer->size())
1198 return -1;
1199
1200 // Uniform block matches slice.vert / slice.frag layout:
1201 // mat4 mvp (64 bytes)
1202 // mat4 sliceToWorld (64 bytes)
1203 // float opacity (4)
1204 // float windowCenter(4)
1205 // float windowWidth (4)
1206 // float _pad0 (4)
1207 struct
1208 {
1209 float mvp[16];
1210 float sliceToWorld[16];
1211 float opacity;
1212 float windowCenter;
1213 float windowWidth;
1214 float _pad0;
1215 } ub;
1216 memcpy(ub.mvp, data.mvp.constData(), 64);
1217 // SliceToWorld is identity for pre-transformed vertices (already in world coords)
1218 QMatrix4x4 identity;
1219 identity.setToIdentity();
1220 memcpy(ub.sliceToWorld, identity.constData(), 64);
1221 ub.opacity = sliceSlot.opacity;
1222 ub.windowCenter = sliceSlot.windowCenter;
1223 ub.windowWidth = sliceSlot.windowWidth;
1224 ub._pad0 = 0.0f;
1225 u->updateDynamicBuffer(k.uniformBuffer.get(), uniformOffset, sizeof(ub), &ub);
1226
1227 return uniformOffset;
1228}
1229
1230//=============================================================================================================
1231
1232void BrainRenderer::issueSliceDraw(QRhiCommandBuffer* cb,
1233 int slotIndex,
1234 int uniformOffset)
1235{
1236 if (slotIndex < 0 || slotIndex >= Impl::kMaxSliceSlots)
1237 return;
1238 if (uniformOffset < 0)
1239 return;
1240
1241 auto& k = d->sliceRes;
1242 if (!k.initialized || !k.pipeline)
1243 return;
1244
1245 cb->setGraphicsPipeline(k.pipeline.get());
1246 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
1247 cb->setShaderResources(k.srb[slotIndex].get(), 1, &srbOffset);
1248 const QRhiCommandBuffer::VertexInput vbuf(k.vertexBuffer[slotIndex].get(), 0);
1249 cb->setVertexInput(0, 1, &vbuf, k.indexBuffer.get(), 0, QRhiCommandBuffer::IndexUInt32);
1250 cb->drawIndexed(6);
1251}
1252
1253//=============================================================================================================
1254
1255void BrainRenderer::renderSurface(QRhiCommandBuffer* cb, QRhi* rhi, const SceneData& data, BrainSurface* surface, ShaderMode mode)
1256{
1257 if (!surface || !surface->isVisible())
1258 return;
1259
1260 auto* pipeline = d->pipelines[mode].get();
1261 if (!pipeline)
1262 return;
1263
1264 // NOTE: Buffer uploads are handled in the pre-render phase
1265 // (BrainView::render pre-upload loop). Do not call
1266 // surface->updateBuffers() here — it would allocate a redundant
1267 // QRhiResourceUpdateBatch per surface.
1268
1269 const int offset = d->claimUniformSlot("renderSurface");
1270 if (offset < 0)
1271 return; // Skip this draw rather than silently corrupt earlier viewport data
1272 QRhiResourceUpdateBatch* u = rhi->nextResourceUpdateBatch();
1273
1274 // On desktop, when a specific annotation region or vertex range is
1275 // selected the CPU vertex-color gold tint (in updateVertexColors)
1276 // provides per-region feedback. Suppress the shader's whole-surface
1277 // gold glow so it doesn't drown out the region highlight.
1278 float selected = surface->isSelected() ? 1.0f : 0.0f;
1279#ifndef __EMSCRIPTEN__
1280 if (surface->isSelected() && (surface->selectedRegionId() != -1 || surface->selectedVertexStart() >= 0)) {
1281 selected = 0.0f;
1282 }
1283#endif
1284
1285 // Pack ALL uniforms into a contiguous block for a single upload
1286 struct
1287 {
1288 float mvp[16]; // 0..63
1289 float cameraPos[3]; // 64..75
1290 float isSelected; // 76..79
1291 float lightDir[3]; // 80..91
1292 float tissueType; // 92..95
1293 float lightingEnabled; // 96..99
1294 float overlayMode; // 100..103
1295 float selectedSurfaceId; // 104..107
1296 } ub;
1297 memcpy(ub.mvp, data.mvp.constData(), 64);
1298 memcpy(ub.cameraPos, &data.cameraPos, 12);
1299 ub.isSelected = selected;
1300 memcpy(ub.lightDir, &data.lightDir, 12);
1301 ub.tissueType = static_cast<float>(surface->tissueType());
1302 ub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
1303 ub.overlayMode = data.overlayMode;
1304 ub.selectedSurfaceId = -1.0f; // Per-surface path: surfaceId selection disabled
1305
1306 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(ub), &ub);
1307
1308 cb->resourceUpdate(u);
1309
1310 // Re-assert the per-pane viewport and scissor after resourceUpdate.
1311 // The scissor provides a hard pixel clip that guarantees no cross-pane
1312 // bleeding, regardless of Metal render-encoder restarts.
1313 cb->setViewport(toViewport(data));
1314 cb->setScissor(toScissor(data));
1315
1316 auto draw = [&](QRhiGraphicsPipeline* p) {
1317 cb->setGraphicsPipeline(p);
1318
1319 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(offset)};
1320 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
1321 const QRhiCommandBuffer::VertexInput vbuf(surface->vertexBuffer(), 0);
1322 cb->setVertexInput(0, 1, &vbuf, surface->indexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
1323 cb->drawIndexed(surface->indexCount());
1324 };
1325
1326 if (mode == Holographic && d->pipelinesBackColor[Holographic]) {
1327 draw(d->pipelinesBackColor[Holographic].get());
1328 }
1329
1330 draw(pipeline);
1331}
1332
1333//=============================================================================================================
1334
1335int BrainRenderer::prepareSurfaceDraw(QRhiResourceUpdateBatch* u,
1336 const SceneData& data,
1337 BrainSurface* surface)
1338{
1339 if (!surface || !surface->isVisible())
1340 return -1;
1341
1342 const int offset = d->claimUniformSlot("prepareSurfaceDraw");
1343 if (offset < 0)
1344 return -1;
1345
1346 float selected = surface->isSelected() ? 1.0f : 0.0f;
1347 if (surface->isSelected() && (surface->selectedRegionId() != -1 || surface->selectedVertexStart() >= 0)) {
1348 selected = 0.0f;
1349 }
1350
1351 struct
1352 {
1353 float mvp[16];
1354 float cameraPos[3];
1355 float isSelected;
1356 float lightDir[3];
1357 float tissueType;
1358 float lightingEnabled;
1359 float overlayMode;
1360 float selectedSurfaceId;
1361 } ub;
1362 memcpy(ub.mvp, data.mvp.constData(), 64);
1363 memcpy(ub.cameraPos, &data.cameraPos, 12);
1364 ub.isSelected = selected;
1365 memcpy(ub.lightDir, &data.lightDir, 12);
1366 ub.tissueType = static_cast<float>(surface->tissueType());
1367 ub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
1368 ub.overlayMode = data.overlayMode;
1369 ub.selectedSurfaceId = -1.0f;
1370
1371 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(ub), &ub);
1372 return offset;
1373}
1374
1375//=============================================================================================================
1376
1377void BrainRenderer::issueSurfaceDraw(QRhiCommandBuffer* cb,
1378 BrainSurface* surface,
1379 ShaderMode mode,
1380 int uniformOffset)
1381{
1382 if (!surface || uniformOffset < 0)
1383 return;
1384
1385 auto* pipeline = d->pipelines[mode].get();
1386 if (!pipeline)
1387 return;
1388
1389 auto draw = [&](QRhiGraphicsPipeline* p) {
1390 cb->setGraphicsPipeline(p);
1391 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(uniformOffset)};
1392 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
1393 const QRhiCommandBuffer::VertexInput vbuf(surface->vertexBuffer(), 0);
1394 cb->setVertexInput(0, 1, &vbuf, surface->indexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
1395 cb->drawIndexed(surface->indexCount());
1396 };
1397
1398 if (mode == Holographic && d->pipelinesBackColor[Holographic]) {
1399 draw(d->pipelinesBackColor[Holographic].get());
1400 }
1401
1402 draw(pipeline);
1403}
1404
1405//=============================================================================================================
1406
1407void BrainRenderer::renderDipoles(QRhiCommandBuffer* cb, QRhi* rhi, const SceneData& data, DipoleObject* dipoles)
1408{
1409 if (!dipoles || !dipoles->isVisible() || dipoles->instanceCount() == 0)
1410 return;
1411
1412 auto* pipeline = d->pipelines[Dipole].get();
1413 if (!pipeline)
1414 return;
1415
1416 QRhiResourceUpdateBatch* u = rhi->nextResourceUpdateBatch();
1417 dipoles->updateBuffers(rhi, u);
1418
1419 // Geometry uploads go through even when the draw is skipped: the object now considers its buffers current
1420 const int offset = d->claimUniformSlot("renderDipoles");
1421 if (offset < 0) {
1422 cb->resourceUpdate(u);
1423 return;
1424 }
1425
1426 // Pack all uniforms into a single contiguous upload
1427 struct
1428 {
1429 float mvp[16]; // 0..63
1430 float cameraPos[3]; // 64..75
1431 float _pad0; // 76..79
1432 float lightDir[3]; // 80..91
1433 float _pad1; // 92..95
1434 float lightingEnabled; // 96..99
1435 } dub;
1436 memcpy(dub.mvp, data.mvp.constData(), 64);
1437 memcpy(dub.cameraPos, &data.cameraPos, 12);
1438 dub._pad0 = 0.0f;
1439 memcpy(dub.lightDir, &data.lightDir, 12);
1440 dub._pad1 = 0.0f;
1441 dub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
1442 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(dub), &dub);
1443
1444 cb->resourceUpdate(u);
1445
1446 // Re-assert the per-pane viewport and scissor.
1447 cb->setViewport(toViewport(data));
1448 cb->setScissor(toScissor(data));
1449
1450 cb->setGraphicsPipeline(pipeline);
1451
1452 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(offset)};
1453 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
1454
1455 const QRhiCommandBuffer::VertexInput bindings[2] = {
1456 QRhiCommandBuffer::VertexInput(dipoles->vertexBuffer(), 0),
1457 QRhiCommandBuffer::VertexInput(dipoles->instanceBuffer(), 0)};
1458
1459 cb->setVertexInput(0, 2, bindings, dipoles->indexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
1460
1461 cb->drawIndexed(dipoles->indexCount(), dipoles->instanceCount());
1462}
1463
1464//=============================================================================================================
1465
1466void BrainRenderer::renderNetwork(QRhiCommandBuffer* cb, QRhi* rhi, const SceneData& data, NetworkObject* network)
1467{
1468 if (!network || !network->isVisible() || !network->hasData())
1469 return;
1470
1471 auto* pipeline = d->pipelines[Dipole].get();
1472 if (!pipeline)
1473 return;
1474
1475 // --- Render Nodes (instanced spheres) ---
1476 if (network->nodeInstanceCount() > 0) {
1477 QRhiResourceUpdateBatch* uNodes = rhi->nextResourceUpdateBatch();
1478 network->updateNodeBuffers(rhi, uNodes);
1479
1480 const int offset = d->claimUniformSlot("renderNetwork (nodes)");
1481 if (offset < 0) {
1482 cb->resourceUpdate(uNodes);
1483 return;
1484 }
1485
1486 struct
1487 {
1488 float mvp[16];
1489 float cp[3];
1490 float _p0;
1491 float ld[3];
1492 float _p1;
1493 float le;
1494 } nub;
1495 memcpy(nub.mvp, data.mvp.constData(), 64);
1496 memcpy(nub.cp, &data.cameraPos, 12);
1497 nub._p0 = 0.0f;
1498 memcpy(nub.ld, &data.lightDir, 12);
1499 nub._p1 = 0.0f;
1500 nub.le = data.lightingEnabled ? 1.0f : 0.0f;
1501 uNodes->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(nub), &nub);
1502
1503 cb->resourceUpdate(uNodes);
1504 cb->setViewport(toViewport(data));
1505 cb->setScissor(toScissor(data));
1506
1507 cb->setGraphicsPipeline(pipeline);
1508
1509 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(offset)};
1510 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
1511
1512 const QRhiCommandBuffer::VertexInput nodeBindings[2] = {
1513 QRhiCommandBuffer::VertexInput(network->nodeVertexBuffer(), 0),
1514 QRhiCommandBuffer::VertexInput(network->nodeInstanceBuffer(), 0)};
1515
1516 cb->setVertexInput(0, 2, nodeBindings, network->nodeIndexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
1517 cb->drawIndexed(network->nodeIndexCount(), network->nodeInstanceCount());
1518 }
1519
1520 // --- Render Edges (instanced cylinders) ---
1521 if (network->edgeInstanceCount() > 0) {
1522 QRhiResourceUpdateBatch* uEdges = rhi->nextResourceUpdateBatch();
1523 network->updateEdgeBuffers(rhi, uEdges);
1524
1525 const int offset = d->claimUniformSlot("renderNetwork (edges)");
1526 if (offset < 0) {
1527 cb->resourceUpdate(uEdges);
1528 return;
1529 }
1530
1531 struct
1532 {
1533 float mvp[16];
1534 float cp[3];
1535 float _p0;
1536 float ld[3];
1537 float _p1;
1538 float le;
1539 } eub;
1540 memcpy(eub.mvp, data.mvp.constData(), 64);
1541 memcpy(eub.cp, &data.cameraPos, 12);
1542 eub._p0 = 0.0f;
1543 memcpy(eub.ld, &data.lightDir, 12);
1544 eub._p1 = 0.0f;
1545 eub.le = data.lightingEnabled ? 1.0f : 0.0f;
1546 uEdges->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(eub), &eub);
1547
1548 cb->resourceUpdate(uEdges);
1549 cb->setViewport(toViewport(data));
1550 cb->setScissor(toScissor(data));
1551
1552 cb->setGraphicsPipeline(pipeline);
1553
1554 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(offset)};
1555 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
1556
1557 const QRhiCommandBuffer::VertexInput edgeBindings[2] = {
1558 QRhiCommandBuffer::VertexInput(network->edgeVertexBuffer(), 0),
1559 QRhiCommandBuffer::VertexInput(network->edgeInstanceBuffer(), 0)};
1560
1561 cb->setVertexInput(0, 2, edgeBindings, network->edgeIndexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
1562 cb->drawIndexed(network->edgeIndexCount(), network->edgeInstanceCount());
1563 }
1564}
1565
1566//=============================================================================================================
1567
1568void BrainRenderer::renderPolyline(QRhiCommandBuffer* cb, QRhi* rhi, const SceneData& data, PolylineObject* polyline)
1569{
1570 if (!polyline || !polyline->isVisible() || !polyline->hasData())
1571 return;
1572
1573 // The segments use the same instance layout as dipoles and network edges,
1574 // so the Dipole pipeline draws them without a shader of their own.
1575 auto* pipeline = d->pipelines[Dipole].get();
1576 if (!pipeline)
1577 return;
1578
1579 QRhiResourceUpdateBatch* u = rhi->nextResourceUpdateBatch();
1580 polyline->updateBuffers(rhi, u);
1581
1582 const int offset = d->claimUniformSlot("renderPolyline");
1583 if (offset < 0) {
1584 cb->resourceUpdate(u);
1585 return;
1586 }
1587
1588 struct
1589 {
1590 float mvp[16];
1591 float cp[3];
1592 float _p0;
1593 float ld[3];
1594 float _p1;
1595 float le;
1596 } ub;
1597 memcpy(ub.mvp, data.mvp.constData(), 64);
1598 memcpy(ub.cp, &data.cameraPos, 12);
1599 ub._p0 = 0.0f;
1600 memcpy(ub.ld, &data.lightDir, 12);
1601 ub._p1 = 0.0f;
1602 ub.le = data.lightingEnabled ? 1.0f : 0.0f;
1603 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(ub), &ub);
1604
1605 cb->resourceUpdate(u);
1606 cb->setViewport(toViewport(data));
1607 cb->setScissor(toScissor(data));
1608
1609 cb->setGraphicsPipeline(pipeline);
1610
1611 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(offset)};
1612 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
1613
1614 const QRhiCommandBuffer::VertexInput bindings[2] = {
1615 QRhiCommandBuffer::VertexInput(polyline->vertexBuffer(), 0),
1616 QRhiCommandBuffer::VertexInput(polyline->instanceBuffer(), 0)};
1617
1618 cb->setVertexInput(0, 2, bindings, polyline->indexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
1619 cb->drawIndexed(polyline->indexCount(), polyline->instanceCount());
1620}
1621
1622//=============================================================================================================
1623// WORKAROUND(QRhi-GLES2): Merged single-drawIndexed rendering.
1624// The Qt QRhi GLES2/WebGL backend has a bug where only the first
1625// drawIndexed() per render pass produces visible output. These two
1626// methods merge all surfaces (brain, BEM, sensors, digitizers,
1627// source-space) into a single VBO/IBO so that all geometry is drawn
1628// in one call.
1629//
1630// Remove when upstream Qt fixes the issue.
1631//=============================================================================================================
1632
1633void BrainRenderer::prepareMergedSurfaces(QRhi* rhi, QRhiResourceUpdateBatch* /*u*/,
1634 const QVector<BrainSurface*>& surfaces,
1635 const QString& groupName)
1636{
1637 auto& group = d->mergedGroups[groupName];
1638
1639 // Check if surface list changed (different count or different pointers)
1640 if (!group.dirty) {
1641 if (group.surfaces.size() != surfaces.size()) {
1642 group.dirty = true;
1643 } else {
1644 for (int i = 0; i < surfaces.size(); ++i) {
1645 if (group.surfaces[i] != surfaces[i]) {
1646 group.dirty = true;
1647 break;
1648 }
1649 }
1650 }
1651 }
1652
1653 // If geometry hasn't changed, check if any surface vertex data actually changed
1654 // (STC animation changes vertex colors but not topology)
1655 if (!group.dirty && group.indexCount > 0) {
1656 // Compare per-surface vertex generation counters
1657 bool anyChanged = false;
1658 if (group.surfaceGenerations.size() != surfaces.size()) {
1659 anyChanged = true;
1660 } else {
1661 for (int i = 0; i < surfaces.size(); ++i) {
1662 if (surfaces[i] && surfaces[i]->vertexGeneration() != group.surfaceGenerations[i]) {
1663 anyChanged = true;
1664 break;
1665 }
1666 }
1667 }
1668
1669 if (!anyChanged) {
1670 // Nothing changed — skip vertex rebuild entirely
1671 return;
1672 }
1673
1674 // Re-merge vertex data directly into vertexRaw (no temp allocation)
1675 // Safety: verify vertex count hasn't changed since the full rebuild.
1676 // If it has, fall through to the full rebuild path to update indices.
1677 int totalVerts = 0;
1678 for (int si = 0; si < surfaces.size(); ++si)
1679 if (surfaces[si])
1680 totalVerts += surfaces[si]->vertexDataRef().size();
1681 if (totalVerts != group.totalVertexCount) {
1682 group.dirty = true;
1683 // Fall through to full rebuild below
1684 } else {
1685 float brainId = 0.0f;
1686 float nonBrainId = 100.0f; // offset so shaders can distinguish
1687 group.surfaceGenerations.resize(surfaces.size());
1688 VertexData* dst = reinterpret_cast<VertexData*>(group.vertexRaw.data());
1689 for (int si = 0; si < surfaces.size(); ++si) {
1690 BrainSurface* surf = surfaces[si];
1691 if (!surf) {
1692 brainId += 1.0f;
1693 nonBrainId += 1.0f;
1694 continue;
1695 }
1696 const bool isBrain = (surf->tissueType() == BrainSurface::TissueBrain);
1697 const float id = isBrain ? brainId : nonBrainId;
1698 const auto& srcVerts = surf->vertexDataRef();
1699 const int n = srcVerts.size();
1700 memcpy(dst, srcVerts.constData(), n * sizeof(VertexData));
1701 for (int j = 0; j < n; ++j)
1702 dst[j].surfaceId = id;
1703 dst += n;
1704 group.surfaceGenerations[si] = surf->vertexGeneration();
1705 brainId += 1.0f;
1706 nonBrainId += 1.0f;
1707 }
1708 group.gpuVertexDirty = true;
1709 return;
1710 }
1711 }
1712
1713 // Full rebuild: topology or surface list changed
1714 group.surfaces = surfaces;
1715 group.indexCount = 0;
1716 group.totalVertexCount = 0;
1717 group.dirty = false;
1718
1719 // Build merged vertex + index arrays
1720 // Pre-calculate total sizes for single allocation
1721 int totalVerts = 0;
1722 int totalIndices = 0;
1723 for (int si = 0; si < surfaces.size(); ++si) {
1724 if (!surfaces[si])
1725 continue;
1726 totalVerts += surfaces[si]->vertexDataRef().size();
1727 totalIndices += surfaces[si]->indexDataRef().size();
1728 }
1729
1730 group.indexCount = totalIndices;
1731 if (group.indexCount == 0)
1732 return;
1733
1734 const quint32 vbufSize = totalVerts * sizeof(VertexData);
1735 const quint32 ibufSize = totalIndices * sizeof(uint32_t);
1736
1737 // (Re-)create Dynamic buffers when they don't exist or are too small
1738 if (!group.vertexBuffer || group.vertexBuffer->size() < vbufSize) {
1739 group.vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1740 QRhiBuffer::VertexBuffer, vbufSize));
1741 group.vertexBuffer->create();
1742 }
1743 if (!group.indexBuffer || group.indexBuffer->size() < ibufSize) {
1744 group.indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
1745 QRhiBuffer::IndexBuffer, ibufSize));
1746 group.indexBuffer->create();
1747 }
1748
1749 // Write directly into QByteArrays — no temp QVector intermediaries
1750 group.vertexRaw.resize(vbufSize);
1751 group.indexRaw.resize(ibufSize);
1752 group.surfaceGenerations.resize(surfaces.size());
1753
1754 VertexData* vDst = reinterpret_cast<VertexData*>(group.vertexRaw.data());
1755 uint32_t* iDst = reinterpret_cast<uint32_t*>(group.indexRaw.data());
1756 float brainId = 0.0f;
1757 float nonBrainId = 100.0f; // offset so shaders can distinguish
1758 uint32_t vertexOffset = 0;
1759 for (int si = 0; si < surfaces.size(); ++si) {
1760 BrainSurface* surf = surfaces[si];
1761 if (!surf) {
1762 brainId += 1.0f;
1763 nonBrainId += 1.0f;
1764 continue;
1765 }
1766
1767 const bool isBrain = (surf->tissueType() == BrainSurface::TissueBrain);
1768 const float id = isBrain ? brainId : nonBrainId;
1769 const auto& srcVerts = surf->vertexDataRef();
1770 const auto& srcIdx = surf->indexDataRef();
1771 const int nv = srcVerts.size();
1772 const int ni = srcIdx.size();
1773
1774 // Bulk copy vertices + stamp surfaceId
1775 memcpy(vDst, srcVerts.constData(), nv * sizeof(VertexData));
1776 for (int j = 0; j < nv; ++j)
1777 vDst[j].surfaceId = id;
1778 vDst += nv;
1779
1780 // Copy indices with global vertex offset
1781 const uint32_t* srcI = srcIdx.constData();
1782 for (int j = 0; j < ni; ++j)
1783 iDst[j] = srcI[j] + vertexOffset;
1784 iDst += ni;
1785
1786 vertexOffset += nv;
1787 group.surfaceGenerations[si] = surf->vertexGeneration();
1788 brainId += 1.0f;
1789 nonBrainId += 1.0f;
1790 }
1791 group.totalVertexCount = totalVerts;
1792 group.gpuVertexDirty = true;
1793 group.gpuIndexDirty = true;
1794}
1795
1796//=============================================================================================================
1797
1798void BrainRenderer::invalidateMergedGroup(const QString& groupName)
1799{
1800 auto it = d->mergedGroups.find(groupName);
1801 if (it != d->mergedGroups.end()) {
1802 it->second.dirty = true;
1803 }
1804}
1805
1806//=============================================================================================================
1807
1808bool BrainRenderer::hasMergedContent(const QString& groupName) const
1809{
1810 auto it = d->mergedGroups.find(groupName);
1811 return it != d->mergedGroups.end() && it->second.indexCount > 0;
1812}
1813
1814//=============================================================================================================
1815
1816void BrainRenderer::drawMergedSurfaces(QRhiCommandBuffer* cb, QRhi* rhi,
1817 const SceneData& data, ShaderMode mode,
1818 const QString& groupName)
1819{
1820 auto it = d->mergedGroups.find(groupName);
1821 if (it == d->mergedGroups.end())
1822 return;
1823 auto& group = it->second;
1824
1825 if (group.indexCount == 0)
1826 return;
1827
1828 auto* pipeline = d->pipelines[mode].get();
1829 if (!pipeline)
1830 return;
1831
1832 // Determine which merged surface (if any) is selected.
1833 // surfaceId encoding: brain surfaces get ids 0,1,2...; non-brain get 100,101,102...
1834 float selectedSurfaceId = -1.0f;
1835 for (int i = 0; i < group.surfaces.size(); ++i) {
1836 if (group.surfaces[i] && group.surfaces[i]->isSelected()) {
1837 if (group.surfaces[i]->selectedRegionId() == -1 && group.surfaces[i]->selectedVertexStart() < 0) {
1838 const bool isBrain = (group.surfaces[i]->tissueType() == BrainSurface::TissueBrain);
1839 selectedSurfaceId = static_cast<float>(isBrain ? i : 100 + i);
1840 }
1841 break;
1842 }
1843 }
1844
1845 QRhiResourceUpdateBatch* u = rhi->nextResourceUpdateBatch();
1846
1847 // Re-upload merged geometry only when data actually changed.
1848 // Split VBO / IBO uploads: the fast-update path (STC color changes)
1849 // only modifies vertices; re-uploading the IBO via glBufferSubData on
1850 // WebGL can corrupt the VAO's element-buffer binding.
1851 if (group.gpuVertexDirty) {
1852 u->updateDynamicBuffer(group.vertexBuffer.get(), 0, group.vertexRaw.size(), group.vertexRaw.constData());
1853 group.gpuVertexDirty = false;
1854 }
1855 if (group.gpuIndexDirty) {
1856 u->updateDynamicBuffer(group.indexBuffer.get(), 0, group.indexRaw.size(), group.indexRaw.constData());
1857 group.gpuIndexDirty = false;
1858 }
1859
1860 const int offset = d->claimUniformSlot("drawMergedSurfaces");
1861 if (offset < 0) {
1862 cb->resourceUpdate(u);
1863 return;
1864 }
1865
1866 // Pack all uniforms into a contiguous block for a single upload
1867 // Layout must match the shader's UniformBlock (std140).
1868 struct
1869 {
1870 float mvp[16]; // 0..63
1871 float cameraPos[3]; // 64..75
1872 float isSelected; // 76..79
1873 float lightDir[3]; // 80..91
1874 float tissueType; // 92..95
1875 float lightingEnabled; // 96..99
1876 float overlayMode; // 100..103
1877 float selectedSurfaceId; // 104..107
1878 } ub;
1879 memcpy(ub.mvp, data.mvp.constData(), 64);
1880 memcpy(ub.cameraPos, &data.cameraPos, 12);
1881 ub.isSelected = 0.0f;
1882 memcpy(ub.lightDir, &data.lightDir, 12);
1883 ub.tissueType = (!group.surfaces.isEmpty() && group.surfaces.first())
1884 ? static_cast<float>(group.surfaces.first()->tissueType())
1885 : 0.0f;
1886 ub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
1887 ub.overlayMode = data.overlayMode;
1888 ub.selectedSurfaceId = selectedSurfaceId;
1889
1890 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(ub), &ub);
1891
1892 cb->resourceUpdate(u);
1893
1894 cb->setViewport(toViewport(data));
1895 cb->setScissor(toScissor(data));
1896
1897 auto draw = [&](QRhiGraphicsPipeline* p) {
1898 cb->setGraphicsPipeline(p);
1899 const QRhiCommandBuffer::DynamicOffset srbOffset = {0, uint32_t(offset)};
1900 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
1901 const QRhiCommandBuffer::VertexInput vbuf(group.vertexBuffer.get(), 0);
1902 cb->setVertexInput(0, 1, &vbuf, group.indexBuffer.get(), 0, QRhiCommandBuffer::IndexUInt32);
1903 cb->drawIndexed(group.indexCount);
1904 };
1905
1906 // WORKAROUND(QRhi-GLES2): On WebGL, only one drawIndexed() per pass.
1907 // For Holographic mode, the back-face pass must happen in a separate
1908 // render pass. The caller is responsible for wrapping each call in
1909 // its own beginPreservingPass/endPass on WASM.
1910#ifdef __EMSCRIPTEN__
1911 draw(pipeline);
1912#else
1913 if (mode == Holographic && d->pipelinesBackColor[Holographic]) {
1914 draw(d->pipelinesBackColor[Holographic].get());
1915 }
1916
1917 draw(pipeline);
1918#endif
1919}
1920
1921} // 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, driven by QRhi instancing.
Renderable cortical / BEM mesh with interleaved vertex attributes and Qt-RHI buffer management.
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.
Interleaved vertex attributes (position, normal, color, curvature) for brain surface GPU upload.
Renderable cortical surface mesh with per-vertex color, curvature data, and GPU buffer management.
quint64 vertexGeneration() const
Monotonically increasing counter bumped whenever vertex data changes.
QRhiBuffer * indexBuffer() const
const QVector< uint32_t > & indexDataRef() const
Const-ref access to CPU-side index data (used by merged rendering).
TissueType tissueType() const
uint32_t indexCount() const
const QVector< VertexData > & vertexDataRef() const
Const-ref access to CPU-side vertex data (used by merged rendering).
QRhiBuffer * vertexBuffer() const
int selectedVertexStart() const
Renderable dipole arrow set with instanced GPU rendering for QRhi.
QRhiBuffer * indexBuffer() const
void updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
QRhiBuffer * vertexBuffer() const
QRhiBuffer * instanceBuffer() const
Renderable network visualization for QRhi.
QRhiBuffer * edgeInstanceBuffer() const
QRhiBuffer * nodeInstanceBuffer() const
QRhiBuffer * nodeIndexBuffer() const
void updateEdgeBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
void updateNodeBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
QRhiBuffer * edgeVertexBuffer() const
QRhiBuffer * edgeIndexBuffer() const
QRhiBuffer * nodeVertexBuffer() const
Connected line segments through an ordered list of points.
void updateBuffers(QRhi *rhi, QRhiResourceUpdateBatch *u)
QRhiBuffer * instanceBuffer() const
QRhiBuffer * indexBuffer() const
QRhiBuffer * vertexBuffer() 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
std::unique_ptr< QRhiRenderBuffer > dsBuffer
std::unique_ptr< QRhiBuffer > uniformBuffer
std::unique_ptr< QRhiTextureRenderTarget > rtClear
void createResources(QRhi *rhi, QRhiRenderPassDescriptor *rp, int sampleCount)
int claimUniformSlot(const char *caller)
static constexpr int kMaxSliceSlots
std::unique_ptr< QRhiTextureRenderTarget > rtPreserve
std::unique_ptr< QRhiShaderResourceBindings > srb
std::unique_ptr< QRhiRenderPassDescriptor > rpClear
std::unique_ptr< QRhiRenderPassDescriptor > rpPreserve
std::map< QString, MergedGroup > mergedGroups
VideoOverlayResources videoOverlay
std::array< std::unique_ptr< QRhiGraphicsPipeline >, kNumShaderModes > pipelinesBackColor
std::array< std::unique_ptr< QRhiGraphicsPipeline >, kNumShaderModes > pipelines
std::unique_ptr< QRhiBuffer > vertexBuffer
std::unique_ptr< QRhiBuffer > indexBuffer
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]
void updateSceneUniforms(QRhi *rhi, const SceneData &data)
static constexpr ShaderMode Dipole
void renderVideoOverlay(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, DISP3DLIB::VideoOverlay *overlay)
void renderVideoOverlayOnSurface(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, DISP3DLIB::VideoOverlay *overlay, BrainSurface *surface)
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 renderDipoles(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, DipoleObject *dipoles)
static constexpr ShaderMode ShowNormals
QRhiRenderTarget * rtPreserve() const
void initialize(QRhi *rhi, QRhiRenderPassDescriptor *rp, int sampleCount)
void prepareMergedSurfaces(QRhi *rhi, QRhiResourceUpdateBatch *u, const QVector< BrainSurface * > &surfaces, const QString &groupName=QStringLiteral("default"))
void issueSurfaceDraw(QRhiCommandBuffer *cb, BrainSurface *surface, ShaderMode mode, int uniformOffset)
QRhiRenderTarget * rtClear() const
bool hasMergedContent(const QString &groupName) const
void invalidateMergedGroup(const QString &groupName=QStringLiteral("default"))
static constexpr ShaderMode Standard
void endPass(QRhiCommandBuffer *cb)
int prepareSurfaceDraw(QRhiResourceUpdateBatch *u, const SceneData &data, BrainSurface *surface)
static constexpr ShaderMode Holographic
static constexpr ShaderMode Anatomical
void renderSurface(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, BrainSurface *surface, ShaderMode mode)
void prepareVideoOverlay(QRhi *rhi, QRhiResourceUpdateBatch *u, DISP3DLIB::VideoOverlay *overlay)
static constexpr ShaderMode XRay
void beginPreservingPass(QRhiCommandBuffer *cb)
void renderNetwork(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, NetworkObject *network)
void renderPolyline(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, PolylineObject *polyline)
void drawMergedSurfaces(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, ShaderMode mode, const QString &groupName=QStringLiteral("default"))
DISP3DLIB::ShaderMode ShaderMode
void beginFrame(QRhiCommandBuffer *cb)
Aggregated GPU resources and render state for the 3-D brain visualization scene.