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