v2.0.0
Loading...
Searching...
No Matches
brainrenderer.cpp
Go to the documentation of this file.
1//=============================================================================================================
36
37//=============================================================================================================
38// INCLUDES
39//=============================================================================================================
40
41#include "brainrenderer.h"
42
43#include <rhi/qrhi.h>
47
48#include <QFile>
49#include <QDebug>
50#include <array>
51#include <map>
52#include <cstring>
53
54//=============================================================================================================
55// PIMPL
56//=============================================================================================================
57
58static constexpr int kNumShaderModes = 6; // Standard..ShowNormals
59
61{
62 void createResources(QRhi *rhi, QRhiRenderPassDescriptor *rp, int sampleCount);
63
64 std::unique_ptr<QRhiShaderResourceBindings> srb;
65
66 // Pipelines for each mode — indexed by ShaderMode enum (O(1) lookup)
67 std::array<std::unique_ptr<QRhiGraphicsPipeline>, kNumShaderModes> pipelines{};
68 std::array<std::unique_ptr<QRhiGraphicsPipeline>, kNumShaderModes> pipelinesBackColor{};
69
70 std::unique_ptr<QRhiBuffer> uniformBuffer;
73
74 bool resourcesDirty = true;
75
76 // ── Dual render targets for multi-pass rendering ─────────────────
77 // Qt bakes load/store flags at create() time, so we need two separate
78 // render targets sharing the same color texture + depth buffer:
79 // - rtClear: clears framebuffer (first pass of each frame)
80 // - rtPreserve: preserves contents (subsequent passes)
81 // Validated in test_wasm_multi_pass on both Metal and WebGL.
82 std::unique_ptr<QRhiRenderBuffer> dsBuffer;
83 std::unique_ptr<QRhiTextureRenderTarget> rtClear;
84 std::unique_ptr<QRhiTextureRenderTarget> rtPreserve;
85 std::unique_ptr<QRhiRenderPassDescriptor> rpClear;
86 std::unique_ptr<QRhiRenderPassDescriptor> rpPreserve;
87 QSize rtSize;
88 QRhiTexture *rtColorTex = nullptr; // Track texture pointer for rebuild
89
90 // ── WORKAROUND(QRhi-GLES2): merged single-drawIndexed buffers ────
91 // Used on WASM to avoid the multi-drawIndexed bug in QRhi's GLES2
92 // backend. Each surface category (brain, BEM, sensors, etc.) gets
93 // its own merged buffer set, drawn in separate render passes.
94 // Remove when upstream Qt fixes the issue.
95 struct MergedGroup {
96 QVector<BrainSurface*> surfaces;
97 std::unique_ptr<QRhiBuffer> vertexBuffer;
98 std::unique_ptr<QRhiBuffer> indexBuffer;
99 int indexCount = 0;
100 int totalVertexCount = 0; // cached vertex count from last full rebuild
101 bool dirty = true; // Geometry needs rebuild (surface list changed)
102 bool gpuVertexDirty = true; // Vertex data changed, needs GPU re-upload
103 bool gpuIndexDirty = true; // Index data changed, needs GPU re-upload
104 QByteArray vertexRaw;
105 QByteArray indexRaw;
106 QVector<quint64> surfaceGenerations; // per-surface vertex generation snapshot
107 };
108 std::map<QString, MergedGroup> mergedGroups; // keyed by category name
109};
110
111//=============================================================================================================
112// Helpers
113//=============================================================================================================
114
115static inline QRhiViewport toViewport(const BrainRenderer::SceneData &d)
116{
117 return QRhiViewport(d.viewportX, d.viewportY, d.viewportW, d.viewportH);
118}
119
120static inline QRhiScissor toScissor(const BrainRenderer::SceneData &d)
121{
122 return QRhiScissor(d.scissorX, d.scissorY, d.scissorW, d.scissorH);
123}
124
125//=============================================================================================================
126// Uniform buffer layout constants — single source of truth for shader ↔ C++ interface
127//=============================================================================================================
128
129namespace {
130 // Uniform buffer sizing
131 constexpr int kUniformSlotCount = 8192; // Max draw calls before overflow
132 constexpr int kUniformBlockSize = 256; // Bound size per SRB dynamic slot (bytes)
133
134 // Per-object uniform byte offsets (must match .vert shader layout)
135 constexpr int kOffsetMVP = 0; // mat4 (64 bytes)
136 constexpr int kOffsetCameraPos = 64; // vec3 (12 bytes)
137 constexpr int kOffsetSelected = 76; // float
138 constexpr int kOffsetLightDir = 80; // vec3 (12 bytes)
139 constexpr int kOffsetTissueType = 92; // float
140 constexpr int kOffsetLighting = 96; // float
141 constexpr int kOffsetOverlayMode = 100; // float
142 constexpr int kOffsetSelectedSurfaceId = 104; // float — WORKAROUND(QRhi-GLES2)
143}
144
145//=============================================================================================================
146// DEFINE MEMBER METHODS
147//=============================================================================================================
148
149//=============================================================================================================
150
152 : d(std::make_unique<Impl>())
153{
154}
155
156//=============================================================================================================
157
159
160//=============================================================================================================
161
162void BrainRenderer::initialize(QRhi *rhi, QRhiRenderPassDescriptor *rp, int sampleCount)
163{
164 if (d->resourcesDirty) {
165 d->createResources(rhi, rp, sampleCount);
166 }
167}
168
169//=============================================================================================================
170
171void BrainRenderer::Impl::createResources(QRhi *rhi, QRhiRenderPassDescriptor *rp, int sampleCount)
172{
173 uniformBufferOffsetAlignment = rhi->ubufAlignment();
174
175 // Create Uniform Buffer
176 if (!uniformBuffer) {
177 // Size for 8192 slots with alignment — enough for 4 viewports × ~1000 surfaces
178 uniformBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, kUniformSlotCount * uniformBufferOffsetAlignment));
179 uniformBuffer->create();
180 }
181
182 // Create SRB
183 if (!srb) {
184 srb.reset(rhi->newShaderResourceBindings());
185 srb->setBindings({
186 // Use dynamic offset for the uniform buffer.
187 // The size of one uniform block in the shader is ~104 bytes,
188 // but we use uniformBufferOffsetAlignment for the stride.
189 QRhiShaderResourceBinding::uniformBufferWithDynamicOffset(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage, uniformBuffer.get(),kUniformBlockSize)
190 });
191 srb->create();
192 }
193
194 // Shader Loader
195 auto getShader = [](const QString &name) {
196 QFile f(name);
197 return f.open(QIODevice::ReadOnly) ? QShader::fromSerialized(f.readAll()) : QShader();
198 };
199
200 // List of modes to initialize
201 QList<ShaderMode> modes = {Standard, Holographic, Anatomical, Dipole, XRay, ShowNormals};
202
203 for (ShaderMode mode : modes) {
204 QString vert = (mode == Holographic || mode == XRay) ? ":/holographic.vert.qsb" :
205 (mode == Anatomical) ? ":/anatomical.vert.qsb" :
206 (mode == Dipole) ? ":/dipole.vert.qsb" :
207 (mode == ShowNormals) ? ":/shownormals.vert.qsb" : ":/standard.vert.qsb";
208
209 QString frag = (mode == Holographic || mode == XRay) ? ":/holographic.frag.qsb" :
210 (mode == Anatomical) ? ":/anatomical.frag.qsb" :
211 (mode == Dipole) ? ":/dipole.frag.qsb" :
212 (mode == ShowNormals) ? ":/shownormals.frag.qsb" : ":/standard.frag.qsb";
213
214 QShader vS = getShader(vert);
215 QShader fS = getShader(frag);
216
217 if (!vS.isValid() || !fS.isValid()) {
218 qWarning() << "BrainRenderer: Could not load shaders for mode" << mode << vert << frag;
219 continue;
220 }
221
222 // Setup Pipeline
223 auto pipeline = std::unique_ptr<QRhiGraphicsPipeline>(rhi->newGraphicsPipeline());
224
225 QRhiGraphicsPipeline::TargetBlend blend;
226 if (mode == Holographic || mode == XRay) {
227 blend.enable = true;
228 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
229 blend.dstColor = QRhiGraphicsPipeline::One;
230 blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha;
231 blend.dstAlpha = QRhiGraphicsPipeline::One;
232 } else if (mode == Dipole) {
233 blend.enable = true;
234 blend.srcColor = QRhiGraphicsPipeline::SrcAlpha;
235 blend.dstColor = QRhiGraphicsPipeline::OneMinusSrcAlpha;
236 blend.srcAlpha = QRhiGraphicsPipeline::SrcAlpha;
237 blend.dstAlpha = QRhiGraphicsPipeline::OneMinusSrcAlpha;
238 }
239
240 auto setup = [&](QRhiGraphicsPipeline* p, QRhiGraphicsPipeline::CullMode cull) {
241 p->setShaderStages({{ QRhiShaderStage::Vertex, vS }, { QRhiShaderStage::Fragment, fS }});
242
243 QRhiVertexInputLayout il;
244
245 if (mode == Dipole) {
246 il.setBindings({
247 { 6 * sizeof(float) }, // Binding 0: Vertex Data (Pos + Normal) -> stride 6 floats
248 { 21 * sizeof(float), QRhiVertexInputBinding::PerInstance } // Binding 1: Instance Data (Mat4 + Color + Selected) -> stride 21 floats
249 });
250
251 il.setAttributes({
252 // Vertex Buffer (Binding 0)
253 { 0, 0, QRhiVertexInputAttribute::Float3, 0 }, // Pos
254 { 0, 1, QRhiVertexInputAttribute::Float3, 3 * sizeof(float) }, // Normal
255
256 // Instance Buffer (Binding 1)
257 // Model Matrix (4 x vec4)
258 { 1, 2, QRhiVertexInputAttribute::Float4, 0 },
259 { 1, 3, QRhiVertexInputAttribute::Float4, 4 * sizeof(float) },
260 { 1, 4, QRhiVertexInputAttribute::Float4, 8 * sizeof(float) },
261 { 1, 5, QRhiVertexInputAttribute::Float4, 12 * sizeof(float) },
262 // Color
263 { 1, 6, QRhiVertexInputAttribute::Float4, 16 * sizeof(float) },
264 // isSelected
265 { 1, 7, QRhiVertexInputAttribute::Float, 20 * sizeof(float) }
266 });
267 } else {
268 il.setBindings({{ 36 }}); // sizeof(VertexData) = 36 with surfaceId
269 il.setAttributes({{ 0, 0, QRhiVertexInputAttribute::Float3, 0 },
270 { 0, 1, QRhiVertexInputAttribute::Float3, 12 },
271 { 0, 2, QRhiVertexInputAttribute::UNormByte4, 24 },
272 { 0, 3, QRhiVertexInputAttribute::UNormByte4, 28 },
273 { 0, 4, QRhiVertexInputAttribute::Float, 32 }}); // surfaceId
274 }
275
276 p->setVertexInputLayout(il);
277 p->setShaderResourceBindings(srb.get());
278 p->setRenderPassDescriptor(rp);
279 p->setSampleCount(sampleCount);
280 p->setCullMode(cull);
281 if (mode == Holographic) {
282 p->setTargetBlends({blend});
283 p->setDepthTest(true);
284 p->setDepthWrite(false);
285 } else if (mode == XRay) {
286 p->setTargetBlends({blend});
287 p->setDepthTest(false); // Disable Depth Test to see through head
288 p->setDepthWrite(false);
289 } else if (mode == Dipole) {
290 p->setTargetBlends({blend});
291 p->setCullMode(QRhiGraphicsPipeline::None);
292 p->setDepthTest(true);
293 p->setDepthWrite(false);
294 } else {
295 p->setDepthTest(true);
296 p->setDepthWrite(true);
297 }
298 p->setFlags(QRhiGraphicsPipeline::UsesScissor);
299 p->create();
300 };
301
302 if (mode == Holographic || mode == XRay) { // Handle XRay back-faces same as Holographic
303 auto pipelineBack = std::unique_ptr<QRhiGraphicsPipeline>(rhi->newGraphicsPipeline());
304 setup(pipelineBack.get(), QRhiGraphicsPipeline::Front);
305 pipelinesBackColor[mode] = std::move(pipelineBack);
306 setup(pipeline.get(), QRhiGraphicsPipeline::Back); // Front faces
307 } else {
308 // Culling: None (Double-sided) to be safe for FreeSurfer meshes
309 setup(pipeline.get(), QRhiGraphicsPipeline::None);
310 }
311 pipelines[mode] = std::move(pipeline);
312 }
313
314 resourcesDirty = false;
315}
316
317//=============================================================================================================
318
319//=============================================================================================================
320
321void BrainRenderer::ensureRenderTargets(QRhi *rhi, QRhiTexture *colorTex, const QSize &pixelSize)
322{
323 // Rebuild when size changes OR when the backing texture changes
324 // (QRhiWidget may return a different colorTexture() each frame).
325 if (d->rtClear && d->rtSize == pixelSize && d->rtColorTex == colorTex)
326 return;
327
328 d->rtSize = pixelSize;
329 d->rtColorTex = colorTex;
330
331 // Shared depth-stencil buffer
332 d->dsBuffer.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize));
333 d->dsBuffer->create();
334
335 QRhiColorAttachment colorAtt(colorTex);
336 QRhiTextureRenderTargetDescription desc(colorAtt);
337 desc.setDepthStencilBuffer(d->dsBuffer.get());
338
339 // RT 1: Clearing (no preserve flags) — used for the first pass of each frame
340 d->rtClear.reset(rhi->newTextureRenderTarget(desc));
341 d->rpClear.reset(d->rtClear->newCompatibleRenderPassDescriptor());
342 d->rtClear->setRenderPassDescriptor(d->rpClear.get());
343 d->rtClear->create();
344
345 // RT 2: Preserving (load previous contents) — used for passes 2+
346 d->rtPreserve.reset(rhi->newTextureRenderTarget(desc,
347 QRhiTextureRenderTarget::PreserveColorContents
348 | QRhiTextureRenderTarget::PreserveDepthStencilContents));
349 d->rpPreserve.reset(d->rtPreserve->newCompatibleRenderPassDescriptor());
350 d->rtPreserve->setRenderPassDescriptor(d->rpPreserve.get());
351 d->rtPreserve->create();
352}
353
354//=============================================================================================================
355
356QRhiRenderTarget *BrainRenderer::rtClear() const
357{
358 return d->rtClear.get();
359}
360
361QRhiRenderTarget *BrainRenderer::rtPreserve() const
362{
363 return d->rtPreserve.get();
364}
365
366//=============================================================================================================
367
368void BrainRenderer::beginFrame(QRhiCommandBuffer *cb)
369{
370 d->currentUniformOffset = 0;
371
372 auto *rt = d->rtClear.get();
373 cb->beginPass(rt, QColor(0, 0, 0), { 1.0f, 0 });
374 const int w = rt->pixelSize().width();
375 const int h = rt->pixelSize().height();
376 cb->setViewport(QRhiViewport(0, 0, w, h));
377 cb->setScissor(QRhiScissor(0, 0, w, h));
378}
379
380//=============================================================================================================
381
383{
384 // NO-OP: packed into per-object slots for simplicity
385}
386
387//=============================================================================================================
388
389void BrainRenderer::beginPreservingPass(QRhiCommandBuffer *cb)
390{
391 auto *rt = d->rtPreserve.get();
392 cb->beginPass(rt, QColor(0, 0, 0), { 1.0f, 0 });
393 const int w = rt->pixelSize().width();
394 const int h = rt->pixelSize().height();
395 cb->setViewport(QRhiViewport(0, 0, w, h));
396 cb->setScissor(QRhiScissor(0, 0, w, h));
397}
398
399//=============================================================================================================
400
401void BrainRenderer::endPass(QRhiCommandBuffer *cb)
402{
403 cb->endPass();
404}
405
406//=============================================================================================================
407
408void BrainRenderer::renderSurface(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, BrainSurface *surface, ShaderMode mode)
409{
410 if (!surface || !surface->isVisible()) return;
411
412 auto *pipeline = d->pipelines[mode].get();
413 if (!pipeline) return;
414
415 // NOTE: Buffer uploads are handled in the pre-render phase
416 // (BrainView::render pre-upload loop). Do not call
417 // surface->updateBuffers() here — it would allocate a redundant
418 // QRhiResourceUpdateBatch per surface.
419
420 QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
421
422 // Dynamic slot update
423 int offset = d->currentUniformOffset;
424 d->currentUniformOffset += d->uniformBufferOffsetAlignment;
425 if (d->currentUniformOffset >= d->uniformBuffer->size()) {
426 qWarning("BrainRenderer: uniform buffer overflow (%d / %d bytes) — too many surfaces. Some draws will be skipped.",
427 d->currentUniformOffset, (int)d->uniformBuffer->size());
428 return; // Skip this draw rather than silently corrupt earlier viewport data
429 }
430
431 // On desktop, when a specific annotation region or vertex range is
432 // selected the CPU vertex-color gold tint (in updateVertexColors)
433 // provides per-region feedback. Suppress the shader's whole-surface
434 // gold glow so it doesn't drown out the region highlight.
435 float selected = surface->isSelected() ? 1.0f : 0.0f;
436#ifndef __EMSCRIPTEN__
437 if (surface->isSelected()
438 && (surface->selectedRegionId() != -1 || surface->selectedVertexStart() >= 0)) {
439 selected = 0.0f;
440 }
441#endif
442
443 // Pack ALL uniforms into a contiguous block for a single upload
444 struct {
445 float mvp[16]; // 0..63
446 float cameraPos[3]; // 64..75
447 float isSelected; // 76..79
448 float lightDir[3]; // 80..91
449 float tissueType; // 92..95
450 float lightingEnabled; // 96..99
451 float overlayMode; // 100..103
452 float selectedSurfaceId;// 104..107
453 } ub;
454 memcpy(ub.mvp, data.mvp.constData(), 64);
455 memcpy(ub.cameraPos, &data.cameraPos, 12);
456 ub.isSelected = selected;
457 memcpy(ub.lightDir, &data.lightDir, 12);
458 ub.tissueType = static_cast<float>(surface->tissueType());
459 ub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
460 ub.overlayMode = data.overlayMode;
461 ub.selectedSurfaceId = -1.0f; // Per-surface path: surfaceId selection disabled
462
463 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(ub), &ub);
464
465 cb->resourceUpdate(u);
466
467 // Re-assert the per-pane viewport and scissor after resourceUpdate.
468 // The scissor provides a hard pixel clip that guarantees no cross-pane
469 // bleeding, regardless of Metal render-encoder restarts.
470 cb->setViewport(toViewport(data));
471 cb->setScissor(toScissor(data));
472
473 auto draw = [&](QRhiGraphicsPipeline *p) {
474 cb->setGraphicsPipeline(p);
475
476 const QRhiCommandBuffer::DynamicOffset srbOffset = { 0, uint32_t(offset) };
477 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
478 const QRhiCommandBuffer::VertexInput vbuf(surface->vertexBuffer(), 0);
479 cb->setVertexInput(0, 1, &vbuf, surface->indexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
480 cb->drawIndexed(surface->indexCount());
481 };
482
483 if (mode == Holographic && d->pipelinesBackColor[Holographic]) {
484 draw(d->pipelinesBackColor[Holographic].get());
485 }
486
487 draw(pipeline);
488}
489
490//=============================================================================================================
491
492int BrainRenderer::prepareSurfaceDraw(QRhiResourceUpdateBatch *u,
493 const SceneData &data,
494 BrainSurface *surface)
495{
496 if (!surface || !surface->isVisible()) return -1;
497
498 int offset = d->currentUniformOffset;
499 d->currentUniformOffset += d->uniformBufferOffsetAlignment;
500 if (d->currentUniformOffset >= d->uniformBuffer->size()) {
501 qWarning("BrainRenderer: uniform buffer overflow in prepareSurfaceDraw");
502 return -1;
503 }
504
505 float selected = surface->isSelected() ? 1.0f : 0.0f;
506 if (surface->isSelected()
507 && (surface->selectedRegionId() != -1 || surface->selectedVertexStart() >= 0)) {
508 selected = 0.0f;
509 }
510
511 struct {
512 float mvp[16];
513 float cameraPos[3];
514 float isSelected;
515 float lightDir[3];
516 float tissueType;
517 float lightingEnabled;
518 float overlayMode;
519 float selectedSurfaceId;
520 } ub;
521 memcpy(ub.mvp, data.mvp.constData(), 64);
522 memcpy(ub.cameraPos, &data.cameraPos, 12);
523 ub.isSelected = selected;
524 memcpy(ub.lightDir, &data.lightDir, 12);
525 ub.tissueType = static_cast<float>(surface->tissueType());
526 ub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
527 ub.overlayMode = data.overlayMode;
528 ub.selectedSurfaceId = -1.0f;
529
530 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(ub), &ub);
531 return offset;
532}
533
534//=============================================================================================================
535
536void BrainRenderer::issueSurfaceDraw(QRhiCommandBuffer *cb,
537 BrainSurface *surface,
538 ShaderMode mode,
539 int uniformOffset)
540{
541 if (!surface || uniformOffset < 0) return;
542
543 auto *pipeline = d->pipelines[mode].get();
544 if (!pipeline) return;
545
546 auto draw = [&](QRhiGraphicsPipeline *p) {
547 cb->setGraphicsPipeline(p);
548 const QRhiCommandBuffer::DynamicOffset srbOffset = { 0, uint32_t(uniformOffset) };
549 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
550 const QRhiCommandBuffer::VertexInput vbuf(surface->vertexBuffer(), 0);
551 cb->setVertexInput(0, 1, &vbuf, surface->indexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
552 cb->drawIndexed(surface->indexCount());
553 };
554
555 if (mode == Holographic && d->pipelinesBackColor[Holographic]) {
556 draw(d->pipelinesBackColor[Holographic].get());
557 }
558
559 draw(pipeline);
560}
561
562//=============================================================================================================
563
564void BrainRenderer::renderDipoles(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, DipoleObject *dipoles)
565{
566 if (!dipoles || !dipoles->isVisible() || dipoles->instanceCount() == 0) return;
567
568 auto *pipeline = d->pipelines[Dipole].get();
569 if (!pipeline) return;
570
571 QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
572 dipoles->updateBuffers(rhi, u);
573
574 // Dynamic slot update
575 int offset = d->currentUniformOffset;
576 d->currentUniformOffset += d->uniformBufferOffsetAlignment;
577 if (d->currentUniformOffset >= d->uniformBuffer->size()) {
578 qWarning("BrainRenderer: uniform buffer overflow in renderDipoles");
579 return;
580 }
581
582 // Pack all uniforms into a single contiguous upload
583 struct {
584 float mvp[16]; // 0..63
585 float cameraPos[3]; // 64..75
586 float _pad0; // 76..79
587 float lightDir[3]; // 80..91
588 float _pad1; // 92..95
589 float lightingEnabled; // 96..99
590 } dub;
591 memcpy(dub.mvp, data.mvp.constData(), 64);
592 memcpy(dub.cameraPos, &data.cameraPos, 12);
593 dub._pad0 = 0.0f;
594 memcpy(dub.lightDir, &data.lightDir, 12);
595 dub._pad1 = 0.0f;
596 dub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
597 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(dub), &dub);
598
599 cb->resourceUpdate(u);
600
601 // Re-assert the per-pane viewport and scissor.
602 cb->setViewport(toViewport(data));
603 cb->setScissor(toScissor(data));
604
605 cb->setGraphicsPipeline(pipeline);
606
607 const QRhiCommandBuffer::DynamicOffset srbOffset = { 0, uint32_t(offset) };
608 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
609
610 const QRhiCommandBuffer::VertexInput bindings[2] = {
611 QRhiCommandBuffer::VertexInput(dipoles->vertexBuffer(), 0),
612 QRhiCommandBuffer::VertexInput(dipoles->instanceBuffer(), 0)
613 };
614
615 cb->setVertexInput(0, 2, bindings, dipoles->indexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
616
617 cb->drawIndexed(dipoles->indexCount(), dipoles->instanceCount());
618}
619
620//=============================================================================================================
621
622void BrainRenderer::renderNetwork(QRhiCommandBuffer *cb, QRhi *rhi, const SceneData &data, NetworkObject *network)
623{
624 if (!network || !network->isVisible() || !network->hasData()) return;
625
626 auto *pipeline = d->pipelines[Dipole].get();
627 if (!pipeline) return;
628
629 // --- Render Nodes (instanced spheres) ---
630 if (network->nodeInstanceCount() > 0) {
631 QRhiResourceUpdateBatch *uNodes = rhi->nextResourceUpdateBatch();
632 network->updateNodeBuffers(rhi, uNodes);
633
634 int offset = d->currentUniformOffset;
635 d->currentUniformOffset += d->uniformBufferOffsetAlignment;
636 if (d->currentUniformOffset >= d->uniformBuffer->size()) {
637 qWarning("BrainRenderer: uniform buffer overflow in renderNetwork (nodes)");
638 return;
639 }
640
641 struct { float mvp[16]; float cp[3]; float _p0; float ld[3]; float _p1; float le; } nub;
642 memcpy(nub.mvp, data.mvp.constData(), 64);
643 memcpy(nub.cp, &data.cameraPos, 12); nub._p0 = 0.0f;
644 memcpy(nub.ld, &data.lightDir, 12); nub._p1 = 0.0f;
645 nub.le = data.lightingEnabled ? 1.0f : 0.0f;
646 uNodes->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(nub), &nub);
647
648 cb->resourceUpdate(uNodes);
649 cb->setViewport(toViewport(data));
650 cb->setScissor(toScissor(data));
651
652 cb->setGraphicsPipeline(pipeline);
653
654 const QRhiCommandBuffer::DynamicOffset srbOffset = { 0, uint32_t(offset) };
655 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
656
657 const QRhiCommandBuffer::VertexInput nodeBindings[2] = {
658 QRhiCommandBuffer::VertexInput(network->nodeVertexBuffer(), 0),
659 QRhiCommandBuffer::VertexInput(network->nodeInstanceBuffer(), 0)
660 };
661
662 cb->setVertexInput(0, 2, nodeBindings, network->nodeIndexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
663 cb->drawIndexed(network->nodeIndexCount(), network->nodeInstanceCount());
664 }
665
666 // --- Render Edges (instanced cylinders) ---
667 if (network->edgeInstanceCount() > 0) {
668 QRhiResourceUpdateBatch *uEdges = rhi->nextResourceUpdateBatch();
669 network->updateEdgeBuffers(rhi, uEdges);
670
671 int offset = d->currentUniformOffset;
672 d->currentUniformOffset += d->uniformBufferOffsetAlignment;
673 if (d->currentUniformOffset >= d->uniformBuffer->size()) {
674 qWarning("BrainRenderer: uniform buffer overflow in renderNetwork (edges)");
675 return;
676 }
677
678 struct { float mvp[16]; float cp[3]; float _p0; float ld[3]; float _p1; float le; } eub;
679 memcpy(eub.mvp, data.mvp.constData(), 64);
680 memcpy(eub.cp, &data.cameraPos, 12); eub._p0 = 0.0f;
681 memcpy(eub.ld, &data.lightDir, 12); eub._p1 = 0.0f;
682 eub.le = data.lightingEnabled ? 1.0f : 0.0f;
683 uEdges->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(eub), &eub);
684
685 cb->resourceUpdate(uEdges);
686 cb->setViewport(toViewport(data));
687 cb->setScissor(toScissor(data));
688
689 cb->setGraphicsPipeline(pipeline);
690
691 const QRhiCommandBuffer::DynamicOffset srbOffset = { 0, uint32_t(offset) };
692 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
693
694 const QRhiCommandBuffer::VertexInput edgeBindings[2] = {
695 QRhiCommandBuffer::VertexInput(network->edgeVertexBuffer(), 0),
696 QRhiCommandBuffer::VertexInput(network->edgeInstanceBuffer(), 0)
697 };
698
699 cb->setVertexInput(0, 2, edgeBindings, network->edgeIndexBuffer(), 0, QRhiCommandBuffer::IndexUInt32);
700 cb->drawIndexed(network->edgeIndexCount(), network->edgeInstanceCount());
701 }
702}
703
704//=============================================================================================================
705// WORKAROUND(QRhi-GLES2): Merged single-drawIndexed rendering.
706// The Qt QRhi GLES2/WebGL backend has a bug where only the first
707// drawIndexed() per render pass produces visible output. These two
708// methods merge all surfaces (brain, BEM, sensors, digitizers,
709// source-space) into a single VBO/IBO so that all geometry is drawn
710// in one call.
711//
712// Remove when upstream Qt fixes the issue.
713//=============================================================================================================
714
715void BrainRenderer::prepareMergedSurfaces(QRhi *rhi, QRhiResourceUpdateBatch * /*u*/,
716 const QVector<BrainSurface*> &surfaces,
717 const QString &groupName)
718{
719 auto &group = d->mergedGroups[groupName];
720
721 // Check if surface list changed (different count or different pointers)
722 if (!group.dirty) {
723 if (group.surfaces.size() != surfaces.size()) {
724 group.dirty = true;
725 } else {
726 for (int i = 0; i < surfaces.size(); ++i) {
727 if (group.surfaces[i] != surfaces[i]) {
728 group.dirty = true;
729 break;
730 }
731 }
732 }
733 }
734
735 // If geometry hasn't changed, check if any surface vertex data actually changed
736 // (STC animation changes vertex colors but not topology)
737 if (!group.dirty && group.indexCount > 0) {
738 // Compare per-surface vertex generation counters
739 bool anyChanged = false;
740 if (group.surfaceGenerations.size() != surfaces.size()) {
741 anyChanged = true;
742 } else {
743 for (int i = 0; i < surfaces.size(); ++i) {
744 if (surfaces[i] && surfaces[i]->vertexGeneration() != group.surfaceGenerations[i]) {
745 anyChanged = true;
746 break;
747 }
748 }
749 }
750
751 if (!anyChanged) {
752 // Nothing changed — skip vertex rebuild entirely
753 return;
754 }
755
756 // Re-merge vertex data directly into vertexRaw (no temp allocation)
757 // Safety: verify vertex count hasn't changed since the full rebuild.
758 // If it has, fall through to the full rebuild path to update indices.
759 int totalVerts = 0;
760 for (int si = 0; si < surfaces.size(); ++si)
761 if (surfaces[si]) totalVerts += surfaces[si]->vertexDataRef().size();
762 if (totalVerts != group.totalVertexCount) {
763 group.dirty = true;
764 // Fall through to full rebuild below
765 } else {
766 float brainId = 0.0f;
767 float nonBrainId = 100.0f; // offset so shaders can distinguish
768 group.surfaceGenerations.resize(surfaces.size());
769 VertexData *dst = reinterpret_cast<VertexData*>(group.vertexRaw.data());
770 for (int si = 0; si < surfaces.size(); ++si) {
771 BrainSurface *surf = surfaces[si];
772 if (!surf) { brainId += 1.0f; nonBrainId += 1.0f; continue; }
773 const bool isBrain = (surf->tissueType() == BrainSurface::TissueBrain);
774 const float id = isBrain ? brainId : nonBrainId;
775 const auto &srcVerts = surf->vertexDataRef();
776 const int n = srcVerts.size();
777 memcpy(dst, srcVerts.constData(), n * sizeof(VertexData));
778 for (int j = 0; j < n; ++j)
779 dst[j].surfaceId = id;
780 dst += n;
781 group.surfaceGenerations[si] = surf->vertexGeneration();
782 brainId += 1.0f;
783 nonBrainId += 1.0f;
784 }
785 group.gpuVertexDirty = true;
786 return;
787 }
788 }
789
790 // Full rebuild: topology or surface list changed
791 group.surfaces = surfaces;
792 group.indexCount = 0;
793 group.totalVertexCount = 0;
794 group.dirty = false;
795
796 // Build merged vertex + index arrays
797 // Pre-calculate total sizes for single allocation
798 int totalVerts = 0;
799 int totalIndices = 0;
800 for (int si = 0; si < surfaces.size(); ++si) {
801 if (!surfaces[si]) continue;
802 totalVerts += surfaces[si]->vertexDataRef().size();
803 totalIndices += surfaces[si]->indexDataRef().size();
804 }
805
806 group.indexCount = totalIndices;
807 if (group.indexCount == 0) return;
808
809 const quint32 vbufSize = totalVerts * sizeof(VertexData);
810 const quint32 ibufSize = totalIndices * sizeof(uint32_t);
811
812 // (Re-)create Dynamic buffers when they don't exist or are too small
813 if (!group.vertexBuffer || group.vertexBuffer->size() < vbufSize) {
814 group.vertexBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
815 QRhiBuffer::VertexBuffer, vbufSize));
816 group.vertexBuffer->create();
817 }
818 if (!group.indexBuffer || group.indexBuffer->size() < ibufSize) {
819 group.indexBuffer.reset(rhi->newBuffer(QRhiBuffer::Dynamic,
820 QRhiBuffer::IndexBuffer, ibufSize));
821 group.indexBuffer->create();
822 }
823
824 // Write directly into QByteArrays — no temp QVector intermediaries
825 group.vertexRaw.resize(vbufSize);
826 group.indexRaw.resize(ibufSize);
827 group.surfaceGenerations.resize(surfaces.size());
828
829 VertexData *vDst = reinterpret_cast<VertexData*>(group.vertexRaw.data());
830 uint32_t *iDst = reinterpret_cast<uint32_t*>(group.indexRaw.data());
831 float brainId = 0.0f;
832 float nonBrainId = 100.0f; // offset so shaders can distinguish
833 uint32_t vertexOffset = 0;
834 for (int si = 0; si < surfaces.size(); ++si) {
835 BrainSurface *surf = surfaces[si];
836 if (!surf) { brainId += 1.0f; nonBrainId += 1.0f; continue; }
837
838 const bool isBrain = (surf->tissueType() == BrainSurface::TissueBrain);
839 const float id = isBrain ? brainId : nonBrainId;
840 const auto &srcVerts = surf->vertexDataRef();
841 const auto &srcIdx = surf->indexDataRef();
842 const int nv = srcVerts.size();
843 const int ni = srcIdx.size();
844
845 // Bulk copy vertices + stamp surfaceId
846 memcpy(vDst, srcVerts.constData(), nv * sizeof(VertexData));
847 for (int j = 0; j < nv; ++j)
848 vDst[j].surfaceId = id;
849 vDst += nv;
850
851 // Copy indices with global vertex offset
852 const uint32_t *srcI = srcIdx.constData();
853 for (int j = 0; j < ni; ++j)
854 iDst[j] = srcI[j] + vertexOffset;
855 iDst += ni;
856
857 vertexOffset += nv;
858 group.surfaceGenerations[si] = surf->vertexGeneration();
859 brainId += 1.0f;
860 nonBrainId += 1.0f;
861 }
862 group.totalVertexCount = totalVerts;
863 group.gpuVertexDirty = true;
864 group.gpuIndexDirty = true;
865}
866
867//=============================================================================================================
868
869void BrainRenderer::invalidateMergedGroup(const QString &groupName)
870{
871 auto it = d->mergedGroups.find(groupName);
872 if (it != d->mergedGroups.end()) {
873 it->second.dirty = true;
874 }
875}
876
877//=============================================================================================================
878
879bool BrainRenderer::hasMergedContent(const QString &groupName) const
880{
881 auto it = d->mergedGroups.find(groupName);
882 return it != d->mergedGroups.end() && it->second.indexCount > 0;
883}
884
885//=============================================================================================================
886
887void BrainRenderer::drawMergedSurfaces(QRhiCommandBuffer *cb, QRhi *rhi,
888 const SceneData &data, ShaderMode mode,
889 const QString &groupName)
890{
891 auto it = d->mergedGroups.find(groupName);
892 if (it == d->mergedGroups.end()) return;
893 auto &group = it->second;
894
895 if (group.indexCount == 0) return;
896
897 auto *pipeline = d->pipelines[mode].get();
898 if (!pipeline) return;
899
900 // Determine which merged surface (if any) is selected.
901 // surfaceId encoding: brain surfaces get ids 0,1,2...; non-brain get 100,101,102...
902 float selectedSurfaceId = -1.0f;
903 for (int i = 0; i < group.surfaces.size(); ++i) {
904 if (group.surfaces[i] && group.surfaces[i]->isSelected()) {
905 if (group.surfaces[i]->selectedRegionId() == -1
906 && group.surfaces[i]->selectedVertexStart() < 0) {
907 const bool isBrain = (group.surfaces[i]->tissueType() == BrainSurface::TissueBrain);
908 selectedSurfaceId = static_cast<float>(isBrain ? i : 100 + i);
909 }
910 break;
911 }
912 }
913
914 QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
915
916 // Re-upload merged geometry only when data actually changed.
917 // Split VBO / IBO uploads: the fast-update path (STC color changes)
918 // only modifies vertices; re-uploading the IBO via glBufferSubData on
919 // WebGL can corrupt the VAO's element-buffer binding.
920 if (group.gpuVertexDirty) {
921 u->updateDynamicBuffer(group.vertexBuffer.get(), 0, group.vertexRaw.size(), group.vertexRaw.constData());
922 group.gpuVertexDirty = false;
923 }
924 if (group.gpuIndexDirty) {
925 u->updateDynamicBuffer(group.indexBuffer.get(), 0, group.indexRaw.size(), group.indexRaw.constData());
926 group.gpuIndexDirty = false;
927 }
928
929 int offset = d->currentUniformOffset;
930 d->currentUniformOffset += d->uniformBufferOffsetAlignment;
931 if (d->currentUniformOffset >= d->uniformBuffer->size()) {
932 qWarning("BrainRenderer: uniform buffer overflow in drawMergedSurfaces");
933 return;
934 }
935
936 // Pack all uniforms into a contiguous block for a single upload
937 // Layout must match the shader's UniformBlock (std140).
938 struct {
939 float mvp[16]; // 0..63
940 float cameraPos[3]; // 64..75
941 float isSelected; // 76..79
942 float lightDir[3]; // 80..91
943 float tissueType; // 92..95
944 float lightingEnabled; // 96..99
945 float overlayMode; // 100..103
946 float selectedSurfaceId;// 104..107
947 } ub;
948 memcpy(ub.mvp, data.mvp.constData(), 64);
949 memcpy(ub.cameraPos, &data.cameraPos, 12);
950 ub.isSelected = 0.0f;
951 memcpy(ub.lightDir, &data.lightDir, 12);
952 ub.tissueType = (!group.surfaces.isEmpty() && group.surfaces.first())
953 ? static_cast<float>(group.surfaces.first()->tissueType()) : 0.0f;
954 ub.lightingEnabled = data.lightingEnabled ? 1.0f : 0.0f;
955 ub.overlayMode = data.overlayMode;
956 ub.selectedSurfaceId = selectedSurfaceId;
957
958 u->updateDynamicBuffer(d->uniformBuffer.get(), offset, sizeof(ub), &ub);
959
960 cb->resourceUpdate(u);
961
962 cb->setViewport(toViewport(data));
963 cb->setScissor(toScissor(data));
964
965 auto draw = [&](QRhiGraphicsPipeline *p) {
966 cb->setGraphicsPipeline(p);
967 const QRhiCommandBuffer::DynamicOffset srbOffset = { 0, uint32_t(offset) };
968 cb->setShaderResources(d->srb.get(), 1, &srbOffset);
969 const QRhiCommandBuffer::VertexInput vbuf(group.vertexBuffer.get(), 0);
970 cb->setVertexInput(0, 1, &vbuf, group.indexBuffer.get(), 0, QRhiCommandBuffer::IndexUInt32);
971 cb->drawIndexed(group.indexCount);
972 };
973
974 // WORKAROUND(QRhi-GLES2): On WebGL, only one drawIndexed() per pass.
975 // For Holographic mode, the back-face pass must happen in a separate
976 // render pass. The caller is responsible for wrapping each call in
977 // its own beginPreservingPass/endPass on WASM.
978#ifdef __EMSCRIPTEN__
979 draw(pipeline);
980#else
981 if (mode == Holographic && d->pipelinesBackColor[Holographic]) {
982 draw(d->pipelinesBackColor[Holographic].get());
983 }
984
985 draw(pipeline);
986#endif
987}
BrainRenderer class declaration.
BrainSurface class declaration.
DipoleObject class declaration.
NetworkObject class declaration.
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
std::unique_ptr< QRhiRenderBuffer > dsBuffer
QRhiTexture * rtColorTex
std::array< std::unique_ptr< QRhiGraphicsPipeline >, kNumShaderModes > pipelinesBackColor
std::map< QString, MergedGroup > mergedGroups
std::unique_ptr< QRhiRenderPassDescriptor > rpPreserve
std::unique_ptr< QRhiShaderResourceBindings > srb
std::unique_ptr< QRhiTextureRenderTarget > rtPreserve
std::unique_ptr< QRhiBuffer > uniformBuffer
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
void endPass(QRhiCommandBuffer *cb)
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 updateSceneUniforms(QRhi *rhi, const SceneData &data)
static constexpr ShaderMode Dipole
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 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.