• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 30213925900

26 Jul 2026 06:03PM UTC coverage: 15.298% (-0.01%) from 15.311%
30213925900

push

github

jcelerier
3rdparty: bump libossia for the network-context poll fix

Brings in ossia/libossia#913: SDL joystick init no longer requires haptic
support (which the Emscripten SDL2 port does not have), and
network_context::poll() restarts the io_context, without which the
WebAssembly polling path stops after its first tick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019oPj1zRcxSQX7FNni7EHM6

30400 of 198713 relevant lines covered (15.3%)

997.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/plugins/score-plugin-gfx/Gfx/Graph/Utils.cpp
1
#include <Gfx/Graph/RenderList.hpp>
2
#include <Gfx/Graph/NodeRenderer.hpp>
3
#include <Gfx/Graph/ShaderCache.hpp>
4
#include <Gfx/Graph/Utils.hpp>
5

6
#include <score/tools/Debug.hpp>
7

8
#if defined(__EMSCRIPTEN__)
9
#include <emscripten/em_asm.h>
10
#include <emscripten/val.h>
11
#endif
12

13
namespace score::gfx
14
{
15
TextureRenderTarget
16
createRenderTarget(const RenderState& state, QRhiTexture* tex, int samples, bool depth, bool samplableDepth)
×
17
{
18
  TextureRenderTarget ret;
×
19
  SCORE_ASSERT(tex);
×
20
  ret.texture = tex;
×
21

22
  // The color "tex" is the resolve target — it is always single-sampled.
23
  //
24
  // When samplable depth is requested alongside MSAA we need depth resolve:
25
  // render into a multisample depth attachment, resolve into a single-sample
26
  // depth texture that downstream shaders can sample. This requires the
27
  // QRhi::ResolveDepthStencil feature, which is supported on Vulkan 1.2+ and
28
  // Metal but NOT on D3D11/12. On unsupported backends we degrade the RT to
29
  // samples=1 — Vulkan/Metal otherwise reject the render pass for mixed
30
  // sample counts across attachments.
31
  int effectiveSamples = samples;
×
32
  bool useDepthResolve = false;
×
33
  if(samplableDepth && samples > 1)
×
34
  {
35
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
36
    useDepthResolve = state.rhi->isFeatureSupported(QRhi::ResolveDepthStencil);
37
#endif
38
    if(!useDepthResolve)
×
39
    {
40
      qWarning() << "createRenderTarget: samplable depth + samples=" << samples
×
41
                 << "but QRhi::ResolveDepthStencil is unsupported on this backend"
×
42
                 << "— degrading this RT to samples=1.";
×
43
      effectiveSamples = 1;
×
44
    }
×
45
  }
×
46

47
  QRhiTextureRenderTargetDescription desc;
×
48
  if(effectiveSamples == 1)
×
49
  {
50
    QRhiColorAttachment color0(tex);
×
51
    desc.setColorAttachments({color0});
×
52
  }
×
53
  else
54
  {
55
    ret.colorRenderBuffer = state.rhi->newRenderBuffer(
×
56
        QRhiRenderBuffer::Color, tex->pixelSize(), effectiveSamples, {}, tex->format());
×
57
    ret.colorRenderBuffer->setName("createRenderTarget::ret.colorRenderBuffer");
×
58
    SCORE_ASSERT(ret.colorRenderBuffer->create());
×
59

60
    QRhiColorAttachment color0(ret.colorRenderBuffer);
×
61
    color0.setResolveTexture(tex);
×
62
    desc.setColorAttachments({color0});
×
63
  }
64
  if(samplableDepth)
×
65
  {
66
    // The single-sample depth texture is what downstream shaders sample.
67
    ret.depthTexture = state.rhi->newTexture(
×
68
        QRhiTexture::D32F, tex->pixelSize(), 1,
×
69
        QRhiTexture::RenderTarget);
×
70
    ret.depthTexture->setName("createRenderTarget::depthTexture");
×
71
    SCORE_ASSERT(ret.depthTexture->create());
×
72

73
    if(useDepthResolve)
×
74
    {
75
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
76
      // Multisample depth attachment used during rendering; resolves into
77
      // ret.depthTexture at endPass(). Owned via ret.msDepthTexture so it
78
      // is released alongside the rest of the RT.
79
      ret.msDepthTexture = state.rhi->newTexture(
80
          QRhiTexture::D32F, tex->pixelSize(), effectiveSamples,
81
          QRhiTexture::RenderTarget);
82
      ret.msDepthTexture->setName("createRenderTarget::msDepthTexture");
83
      SCORE_ASSERT(ret.msDepthTexture->create());
84

85
      desc.setDepthTexture(ret.msDepthTexture);
86
      desc.setDepthResolveTexture(ret.depthTexture);
87
#endif
88
    }
×
89
    else
90
    {
91
      desc.setDepthTexture(ret.depthTexture);
×
92
    }
93
  }
×
94
  else if(depth)
×
95
  {
96
    ret.depthRenderBuffer = state.rhi->newRenderBuffer(
×
97
        QRhiRenderBuffer::DepthStencil, tex->pixelSize(), effectiveSamples);
×
98
    ret.depthRenderBuffer->setName("createRenderTarget::ret.depthRenderBuffer");
×
99
    SCORE_ASSERT(ret.depthRenderBuffer->create());
×
100

101
    desc.setDepthStencilBuffer(ret.depthRenderBuffer);
×
102
  }
×
103

104
  auto renderTarget = state.rhi->newTextureRenderTarget(desc);
×
105
  renderTarget->setName("createRenderTarget::renderTarget");
×
106
  SCORE_ASSERT(renderTarget);
×
107

108
  auto renderPass = renderTarget->newCompatibleRenderPassDescriptor();
×
109
  renderPass->setName("createRenderTarget::renderPass");
×
110
  SCORE_ASSERT(renderPass);
×
111

112
  renderTarget->setRenderPassDescriptor(renderPass);
×
113
  SCORE_ASSERT(renderTarget->create());
×
114

115
  ret.renderTarget = renderTarget;
×
116
  ret.renderPass = renderPass;
×
117
  return ret;
×
118
}
×
119

120
TextureRenderTarget createRenderTarget(
×
121
    const RenderState& state, QRhiTexture::Format fmt, QSize sz, int samples, bool depth,
122
    bool samplableDepth, QRhiTexture::Flags flags)
123
{
124
  // FIXME not every RT needs mipmap / generatemips
125
  auto texture = state.rhi->newTexture(
×
126
      fmt, sz, 1,
×
127
      QRhiTexture::RenderTarget | QRhiTexture::UsedWithLoadStore | QRhiTexture::MipMapped
×
128
          | QRhiTexture::UsedWithGenerateMips | flags);
×
129
  texture->setName("createRenderTarget::texture");
×
130
  SCORE_ASSERT(texture->create());
×
131
  return createRenderTarget(state, texture, samples, depth, samplableDepth);
×
132
}
×
133

134
TextureRenderTarget createRenderTarget(
×
135
    const RenderState& state,
136
    std::span<QRhiTexture* const> colorTextures,
137
    QRhiTexture* depthTex,
138
    int samples)
139
{
140
  TextureRenderTarget ret;
×
141
  SCORE_ASSERT(!colorTextures.empty());
×
142

143
  ret.texture = colorTextures[0];
×
144
  for(std::size_t i = 1; i < colorTextures.size(); i++)
×
145
    ret.additionalColorTextures.push_back(colorTextures[i]);
×
146

147
  // depthTex is the single-sample resolve target supplied by the caller; if
148
  // MSAA is requested we need depth-resolve support to keep both. Without it
149
  // (e.g. D3D11/12) all attachments must share a sample count, so degrade
150
  // this RT to samples=1 — see the matching comment in the non-MRT overload.
151
  int effectiveSamples = samples;
×
152
  bool useDepthResolve = false;
×
153
  if(depthTex && samples > 1)
×
154
  {
155
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
156
    useDepthResolve = state.rhi->isFeatureSupported(QRhi::ResolveDepthStencil);
157
#endif
158
    if(!useDepthResolve)
×
159
    {
160
      qWarning() << "createRenderTarget(MRT): samplable depth + samples=" << samples
×
161
                 << "but QRhi::ResolveDepthStencil is unsupported on this backend"
×
162
                 << "— degrading this RT to samples=1.";
×
163
      effectiveSamples = 1;
×
164
    }
×
165
  }
×
166

167
  QList<QRhiColorAttachment> attachments;
×
168
  for(auto* tex : colorTextures)
×
169
  {
170
    if(effectiveSamples == 1)
×
171
    {
172
      attachments.append(QRhiColorAttachment(tex));
×
173
    }
×
174
    else
175
    {
176
      auto* rb = state.rhi->newRenderBuffer(
×
177
          QRhiRenderBuffer::Color, tex->pixelSize(), effectiveSamples, {}, tex->format());
×
178
      rb->setName("createRenderTarget::MRT::colorRB");
×
179
      SCORE_ASSERT(rb->create());
×
180

181
      QRhiColorAttachment att(rb);
×
182
      att.setResolveTexture(tex);
×
183
      attachments.append(att);
×
184
    }
185
  }
186

187
  QRhiTextureRenderTargetDescription desc;
×
188
  desc.setColorAttachments(attachments.begin(), attachments.end());
×
189

190
  if(depthTex)
×
191
  {
192
    ret.depthTexture = depthTex;
×
193
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
194
    if(useDepthResolve)
195
    {
196
#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
197
      // Multisample depth attachment used during rendering, resolves into
198
      // the caller-supplied depthTex on endPass(). We own msDepthTexture.
199
      ret.msDepthTexture = state.rhi->newTexture(
200
          QRhiTexture::D32F, depthTex->pixelSize(), effectiveSamples,
201
          QRhiTexture::RenderTarget);
202
      ret.msDepthTexture->setName("createRenderTarget::MRT::msDepthTexture");
203
      SCORE_ASSERT(ret.msDepthTexture->create());
204

205
      desc.setDepthTexture(ret.msDepthTexture);
206
      desc.setDepthResolveTexture(depthTex);
207
#endif
208
    }
209
    else
210
    {
211
      desc.setDepthTexture(depthTex);
212
    }
213
#else
214
    // Qt < 6.6 doesn't support sampleable depth textures in render targets;
215
    // fall back to a depth renderbuffer (depth won't be sampleable)
216
    ret.depthRenderBuffer = state.rhi->newRenderBuffer(
×
217
        QRhiRenderBuffer::DepthStencil, colorTextures[0]->pixelSize(), effectiveSamples, {},
×
218
        QRhiTexture::D32F);
219
    ret.depthRenderBuffer->setName("createRenderTarget::MRT::depthRB_fallback");
×
220
    SCORE_ASSERT(ret.depthRenderBuffer->create());
×
221
    desc.setDepthStencilBuffer(ret.depthRenderBuffer);
×
222
#endif
223
  }
×
224

225
  auto renderTarget = state.rhi->newTextureRenderTarget(desc);
×
226
  renderTarget->setName("createRenderTarget::MRT::renderTarget");
×
227
  SCORE_ASSERT(renderTarget);
×
228

229
  auto renderPass = renderTarget->newCompatibleRenderPassDescriptor();
×
230
  renderPass->setName("createRenderTarget::MRT::renderPass");
×
231
  SCORE_ASSERT(renderPass);
×
232

233
  renderTarget->setRenderPassDescriptor(renderPass);
×
234
  SCORE_ASSERT(renderTarget->create());
×
235

236
  ret.renderTarget = renderTarget;
×
237
  ret.renderPass = renderPass;
×
238
  return ret;
×
239
}
×
240

241
void replaceBuffer(
×
242
    std::vector<QRhiShaderResourceBinding>& tmp, int binding, QRhiBuffer* newBuffer)
243
{
244
  // const auto bufType = newBuffer->resourceType();
245
  for(QRhiShaderResourceBinding& b : tmp)
×
246
  {
247
    auto d = reinterpret_cast<QRhiShaderResourceBinding::Data*>(&b);
×
248
    if(d->binding == binding)
×
249
    {
250
      switch(d->type)
×
251
      {
252
        case QRhiShaderResourceBinding::Type::UniformBuffer:
253
          d->u.ubuf.buf = newBuffer;
×
254
          break;
×
255
        case QRhiShaderResourceBinding::Type::BufferLoad:
256
        case QRhiShaderResourceBinding::Type::BufferStore:
257
        case QRhiShaderResourceBinding::Type::BufferLoadStore:
258
          d->u.sbuf.buf = newBuffer;
×
259
          break;
×
260
        default:
261
          break;
×
262
      }
263
    }
×
264
  }
265
}
×
266

267
void replaceSampler(
×
268
    std::vector<QRhiShaderResourceBinding>& tmp, int binding, QRhiSampler* newSampler)
269
{
270
  for(QRhiShaderResourceBinding& b : tmp)
×
271
  {
272
    auto d = reinterpret_cast<QRhiShaderResourceBinding::Data*>(&b);
×
273
    if(d->binding == binding)
×
274
    {
275
      if(d->type == QRhiShaderResourceBinding::Type::SampledTexture)
×
276
      {
277
        d->u.stex.texSamplers[0].sampler = newSampler;
×
278
      }
×
279
    }
×
280
  }
281
}
×
282

283
void replaceTexture(
×
284
    std::vector<QRhiShaderResourceBinding>& tmp, int binding, QRhiTexture* newTexture)
285
{
286
  for(QRhiShaderResourceBinding& b : tmp)
×
287
  {
288
    auto d = reinterpret_cast<QRhiShaderResourceBinding::Data*>(&b);
×
289
    if(d->binding == binding)
×
290
    {
291
      switch(d->type)
×
292
      {
293
        case QRhiShaderResourceBinding::Type::SampledTexture:
294
          d->u.stex.texSamplers[0].tex = newTexture;
×
295
          break;
×
296
        case QRhiShaderResourceBinding::Type::ImageLoad:
297
        case QRhiShaderResourceBinding::Type::ImageStore:
298
        case QRhiShaderResourceBinding::Type::ImageLoadStore:
299
          d->u.simage.tex = newTexture;
×
300
          break;
×
301
        default:
302
          break;
×
303
      }
304
    }
×
305
  }
306
}
×
307

308
void replaceBuffer(QRhiShaderResourceBindings& srb, int binding, QRhiBuffer* newBuffer)
×
309
{
310
  std::vector<QRhiShaderResourceBinding> tmp;
×
311
  tmp.assign(srb.cbeginBindings(), srb.cendBindings());
×
312

313
  replaceBuffer(tmp, binding, newBuffer);
×
314

315
  srb.destroy();
×
316
  srb.setBindings(tmp.begin(), tmp.end());
×
317
  srb.create();
×
318
}
×
319

320
void replaceSampler(
×
321
    QRhiShaderResourceBindings& srb, int binding, QRhiSampler* newSampler)
322
{
323
  std::vector<QRhiShaderResourceBinding> tmp;
×
324
  tmp.assign(srb.cbeginBindings(), srb.cendBindings());
×
325

326
  replaceSampler(tmp, binding, newSampler);
×
327

328
  srb.destroy();
×
329
  srb.setBindings(tmp.begin(), tmp.end());
×
330
  srb.create();
×
331
}
×
332

333
void replaceTexture(
×
334
    QRhiShaderResourceBindings& srb, int binding, QRhiTexture* newTexture)
335
{
336
  std::vector<QRhiShaderResourceBinding> tmp;
×
337
  tmp.assign(srb.cbeginBindings(), srb.cendBindings());
×
338

339
  replaceTexture(tmp, binding, newTexture);
×
340

341
  srb.destroy();
×
342
  srb.setBindings(tmp.begin(), tmp.end());
×
343
  srb.create();
×
344
}
×
345

346
void replaceSampler(
×
347
    QRhiShaderResourceBindings& srb, QRhiSampler* oldSampler, QRhiSampler* newSampler)
348
{
349
  std::vector<QRhiShaderResourceBinding> tmp;
×
350
  tmp.assign(srb.cbeginBindings(), srb.cendBindings());
×
351
  for(QRhiShaderResourceBinding& b : tmp)
×
352
  {
353
    auto d = reinterpret_cast<QRhiShaderResourceBinding::Data*>(&b);
×
354
    if(d->type == QRhiShaderResourceBinding::Type::SampledTexture)
×
355
    {
356
      SCORE_ASSERT(d->u.stex.count >= 1);
×
357
      if(d->u.stex.texSamplers[0].sampler == oldSampler)
×
358
      {
359
        d->u.stex.texSamplers[0].sampler = newSampler;
×
360
      }
×
361
    }
×
362
  }
363

364
  srb.destroy();
×
365
  srb.setBindings(tmp.begin(), tmp.end());
×
366
  srb.create();
×
367
}
×
368

369
void replaceSamplerAndTexture(
×
370
    QRhiShaderResourceBindings& srb, QRhiSampler* oldSampler, QRhiSampler* newSampler,
371
    QRhiTexture* newTexture)
372
{
373
  std::vector<QRhiShaderResourceBinding> tmp;
×
374
  tmp.assign(srb.cbeginBindings(), srb.cendBindings());
×
375
  for(QRhiShaderResourceBinding& b : tmp)
×
376
  {
377
    auto d = reinterpret_cast<QRhiShaderResourceBinding::Data*>(&b);
×
378
    if(d->type == QRhiShaderResourceBinding::Type::SampledTexture)
×
379
    {
380
      SCORE_ASSERT(d->u.stex.count >= 1);
×
381
      if(d->u.stex.texSamplers[0].sampler == oldSampler)
×
382
      {
383
        d->u.stex.texSamplers[0].sampler = newSampler;
×
384
        d->u.stex.texSamplers[0].tex = newTexture;
×
385
      }
×
386
    }
×
387
  }
388

389
  srb.destroy();
×
390
  srb.setBindings(tmp.begin(), tmp.end());
×
391
  srb.create();
×
392
}
×
393

394
void replaceTexture(
×
395
    QRhiShaderResourceBindings& srb, QRhiSampler* sampler, QRhiTexture* newTexture)
396
{
397
  std::vector<QRhiShaderResourceBinding> tmp;
×
398
  tmp.assign(srb.cbeginBindings(), srb.cendBindings());
×
399
  for(QRhiShaderResourceBinding& b : tmp)
×
400
  {
401
    auto d = reinterpret_cast<QRhiShaderResourceBinding::Data*>(&b);
×
402
    if(d->type == QRhiShaderResourceBinding::Type::SampledTexture)
×
403
    {
404
      SCORE_ASSERT(d->u.stex.count >= 1);
×
405
      if(d->u.stex.texSamplers[0].sampler == sampler)
×
406
      {
407
        d->u.stex.texSamplers[0].tex = newTexture;
×
408
      }
×
409
    }
×
410
  }
411

412
  srb.destroy();
×
413
  srb.setBindings(tmp.begin(), tmp.end());
×
414
  srb.create();
×
415
}
×
416

417
void replaceTexture(
×
418
    QRhiShaderResourceBindings& srb, QRhiTexture* old_tex, QRhiTexture* new_tex)
419
{
420
  QVarLengthArray<QRhiShaderResourceBinding> bindings;
×
421
  for(auto it = srb.cbeginBindings(); it != srb.cendBindings(); ++it)
×
422
  {
423
    bindings.push_back(*it);
×
424

425
    auto& binding = bindings.back();
×
426

427
    auto& d = *reinterpret_cast<QRhiShaderResourceBinding::Data*>(&binding);
×
428
    if(d.type == QRhiShaderResourceBinding::SampledTexture)
×
429
    {
430
      if(d.u.stex.texSamplers[0].tex == old_tex)
×
431
      {
432
        d.u.stex.texSamplers[0].tex = new_tex;
×
433
      }
×
434
    }
×
435
  }
×
436
  srb.destroy();
×
437
  srb.setBindings(bindings.begin(), bindings.end());
×
438
  srb.create();
×
439
}
×
440

441
bool remapPipelineVertexInputs(
×
442
    QRhiGraphicsPipeline& pip, const QShader& vertexShader,
443
    const ossia::geometry& geom)
444
{
445
  const auto& shader_inputs = vertexShader.description().inputVariables();
×
446
  if(shader_inputs.empty())
×
447
    return true;
×
448

449
  QVarLengthArray<QRhiVertexInputAttribute> remappedAttrs;
×
450

451
  for(const auto& shader_var : shader_inputs)
×
452
  {
453
    // Resolve shader variable name to semantic
454
    const std::string_view var_name(shader_var.name.constData(), shader_var.name.size());
×
455
    auto sem = ossia::name_to_semantic(var_name);
×
456

457
    // Find matching geometry attribute: by semantic, then custom name, then display name
458
    const ossia::geometry::attribute* match = nullptr;
×
459
    if(sem != ossia::attribute_semantic::custom)
×
460
      match = geom.find(sem);
×
461
    if(!match)
×
462
      match = geom.find(var_name);
×
463
    if(!match)
×
464
    {
465
      // Fallback: match shader variable name against attribute display names
466
      for(const auto& a : geom.attributes)
×
467
      {
468
        if(ossia::geometry::display_name(a) == var_name)
×
469
        {
470
          match = &a;
×
471
          break;
×
472
        }
473
      }
474
    }
×
475

476
    if(!match)
×
477
      return false;
×
478

479
    // binding/format/offset from GEOMETRY, location from SHADER
480
    remappedAttrs.append(QRhiVertexInputAttribute(
×
481
        match->binding, shader_var.location,
×
482
        static_cast<QRhiVertexInputAttribute::Format>(match->format),
×
483
        match->byte_offset));
×
484
  }
485

486
  // Override vertex input layout, keeping the bindings (stride/classification)
487
  QRhiVertexInputLayout inputLayout;
×
488
  const auto& prevLayout = pip.vertexInputLayout();
×
489
  inputLayout.setBindings(prevLayout.cbeginBindings(), prevLayout.cendBindings());
×
490
  inputLayout.setAttributes(remappedAttrs.begin(), remappedAttrs.end());
×
491
  pip.setVertexInputLayout(inputLayout);
×
492
  return true;
×
493
}
×
494

495
Pipeline buildPipeline(
×
496
    const RenderList& renderer, const Mesh& mesh, const QShader& vertexS,
497
    const QShader& fragmentS, const TextureRenderTarget& rt,
498
    QRhiShaderResourceBindings* srb)
499
{
500
  auto& rhi = *renderer.state.rhi;
×
501
  auto ps = rhi.newGraphicsPipeline();
×
502
  ps->setName("buildPipeline::ps");
×
503
  SCORE_ASSERT(ps);
×
504

505
  QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
×
506
  premulAlphaBlend.enable = true;
×
507
  premulAlphaBlend.srcColor = QRhiGraphicsPipeline::BlendFactor::SrcAlpha;
×
508
  premulAlphaBlend.dstColor = QRhiGraphicsPipeline::BlendFactor::OneMinusSrcAlpha;
×
509
  premulAlphaBlend.srcAlpha = QRhiGraphicsPipeline::BlendFactor::SrcAlpha;
×
510
  premulAlphaBlend.dstAlpha = QRhiGraphicsPipeline::BlendFactor::OneMinusSrcAlpha;
×
511

512
  // MRT: one blend state per color attachment
513
  int numColorAttachments = rt.colorAttachmentCount();
×
514
  QList<QRhiGraphicsPipeline::TargetBlend> blends;
×
515
  for(int i = 0; i < std::max(1, numColorAttachments); i++)
×
516
    blends.append(premulAlphaBlend);
×
517
  ps->setTargetBlends(blends.begin(), blends.end());
×
518

519
  // Use the render target's actual sample count whenever it can be queried,
520
  // NOT renderer.samples(). The two can differ when an RT was degraded
521
  // (e.g. samplable-depth + MSAA without depth-resolve support) — in that
522
  // case the pipeline must agree with the RT or Vulkan will reject the
523
  // render pass. When only renderPass is set (e.g. MultiWindowNode passes
524
  // a placeholder rt that targets a swap chain), sampleCount() returns -1
525
  // and we have to trust the renderlist value.
526
  const int rtSamplesQueried = rt.sampleCount();
×
527
  const int pipelineSamples = (rtSamplesQueried > 0) ? rtSamplesQueried : renderer.samples();
×
528
  if(rtSamplesQueried > 0 && rtSamplesQueried != renderer.samples())
×
529
  {
530
    qWarning() << "buildPipeline: RT sampleCount=" << rtSamplesQueried
×
531
               << "differs from renderer.samples()=" << renderer.samples()
×
532
               << "— pipeline will use" << pipelineSamples;
×
533
  }
×
534
  ps->setSampleCount(pipelineSamples);
×
535

536
  mesh.preparePipeline(*ps);
×
537

538
  // Remap vertex inputs by semantic if the mesh provides semantic geometry.
539
  // This matches shader input variable names to geometry attribute semantics,
540
  // so that locations are determined by the shader, not by the geometry producer.
541
  if(auto* geom = mesh.semanticGeometry())
×
542
  {
543
    if(!remapPipelineVertexInputs(*ps, vertexS, *geom))
×
544
    {
545
      qDebug() << "Warning! Shader requires attributes not present in mesh";
×
546
      delete ps;
×
547
      return {nullptr, srb};
×
548
    }
549
  }
×
550

551
  // FIXME does that check make sense?
552
  if(!renderer.anyNodeRequiresDepth())
×
553
  {
554
    ps->setDepthTest(false);
×
555
    ps->setDepthWrite(false);
×
556
  }
×
557

558
  ps->setShaderStages(
×
559
      {{QRhiShaderStage::Vertex, vertexS}, {QRhiShaderStage::Fragment, fragmentS}});
×
560

561
  ps->setShaderResourceBindings(srb);
×
562

563
  SCORE_ASSERT(rt.renderPass);
×
564
  ps->setRenderPassDescriptor(rt.renderPass);
×
565

566
  if(!ps->create())
×
567
  {
568
    qDebug() << "Warning! Pipeline not created";
×
569
    delete ps;
×
570
    ps = nullptr;
×
571
  }
×
572
  return {ps, srb};
×
573
}
×
574

575
QRhiShaderResourceBindings* createDefaultBindings(
×
576
    const RenderList& renderer, const TextureRenderTarget& rt, QRhiBuffer* processUBO,
577
    QRhiBuffer* materialUBO, std::span<const Sampler> samplers,
578
    std::span<QRhiShaderResourceBinding> additionalBindings)
579
{
580
  auto& rhi = *renderer.state.rhi;
×
581
  // Shader resource bindings
582
  auto srb = rhi.newShaderResourceBindings();
×
583
  SCORE_ASSERT(srb);
×
584

585
  QVarLengthArray<QRhiShaderResourceBinding, 8> bindings;
×
586

587
  const auto bindingStages = QRhiShaderResourceBinding::VertexStage
588
                             | QRhiShaderResourceBinding::FragmentStage;
×
589

590
  {
591
    const auto rendererBinding = QRhiShaderResourceBinding::uniformBuffer(
×
592
        0, bindingStages, &renderer.outputUBO());
×
593
    bindings.push_back(rendererBinding);
×
594
  }
595

596
  if(processUBO)
×
597
  {
598
    const auto standardUniformBinding
599
        = QRhiShaderResourceBinding::uniformBuffer(1, bindingStages, processUBO);
×
600
    bindings.push_back(standardUniformBinding);
×
601
  }
×
602

603
  // Bind materials
604
  if(materialUBO)
×
605
  {
606
    const auto materialBinding
607
        = QRhiShaderResourceBinding::uniformBuffer(2, bindingStages, materialUBO);
×
608
    bindings.push_back(materialBinding);
×
609
  }
×
610

611
  // Bind samplers
612
  int binding = 3;
×
613
  for(auto sampler : samplers)
×
614
  {
615
    assert(sampler.texture);
×
616
    auto actual_texture = sampler.texture;
×
617

618
    // For cases where we do multi-pass rendering, set "this pass"'s input texture
619
    // to an empty texture instead as we can't output to an input texture
620
    if(actual_texture == rt.texture)
×
621
      actual_texture = &renderer.emptyTexture();
×
622

623
    bindings.push_back(QRhiShaderResourceBinding::sampledTexture(
×
624
        binding,
×
625
        QRhiShaderResourceBinding::VertexStage
626
            | QRhiShaderResourceBinding::FragmentStage,
×
627
        actual_texture, sampler.sampler));
×
628
    binding++;
×
629
  }
630

631
  for(auto& other : additionalBindings)
×
632
  {
633
    bindings.push_back(other);
×
634
  }
635

636
  srb->setBindings(bindings.begin(), bindings.end());
×
637
  SCORE_ASSERT(srb->create());
×
638
  return srb;
×
639
}
×
640

641
Pipeline buildPipeline(
×
642
    const RenderList& renderer, const Mesh& mesh, const QShader& vertexS,
643
    const QShader& fragmentS, const TextureRenderTarget& rt, QRhiBuffer* processUBO,
644
    QRhiBuffer* materialUBO, std::span<const Sampler> samplers,
645
    std::span<QRhiShaderResourceBinding> additionalBindings)
646
{
647
  auto bindings = createDefaultBindings(
×
648
      renderer, rt, processUBO, materialUBO, samplers, additionalBindings);
×
649
  return buildPipeline(renderer, mesh, vertexS, fragmentS, rt, bindings);
×
650
}
651

652
std::pair<QShader, QShader> makeShaders(const RenderState& v, QString vert, QString frag)
×
653
{
654
  auto [vertexS, vertexError] = ShaderCache::get(v, vert.toUtf8(), QShader::VertexStage);
×
655
  if(!vertexError.isEmpty())
×
656
  {
657
    qDebug() << vertexError;
×
658
    qDebug() << vert.toStdString().data();
×
659
  }
×
660

661
  auto [fragmentS, fragmentError]
×
662
      = ShaderCache::get(v, frag.toUtf8(), QShader::FragmentStage);
×
663
  if(!fragmentError.isEmpty())
×
664
  {
665
    qDebug() << fragmentError;
×
666
    qDebug() << frag.toStdString().data();
×
667
  }
×
668

669
  // qDebug().noquote() << vert.toUtf8().constData();
670
  if(!vertexS.isValid())
×
671
    throw std::runtime_error("invalid vertex shader");
×
672
  if(!fragmentS.isValid())
×
673
    throw std::runtime_error("invalid fragment shader");
×
674

675
  return {vertexS, fragmentS};
×
676
}
×
677

678
// TODO move to ShaderCache
679
QShader makeCompute(const RenderState& v, QString compute)
×
680
{
681
  auto [computeS, computeError]
×
682
      = ShaderCache::get(v, compute.toUtf8(), QShader::ComputeStage);
×
683
  if(!computeError.isEmpty())
×
684
    qDebug() << computeError;
×
685

686
  if(!computeS.isValid())
×
687
    throw std::runtime_error("invalid compute shader");
×
688
  return computeS;
×
689
}
×
690

691
void DefaultShaderMaterial::init(
×
692
    RenderList& renderer, const std::vector<Port*>& input,
693
    ossia::small_vector<Sampler, 8>& samplers)
694
{
695
  auto& rhi = *renderer.state.rhi;
×
696

697
  // Set up shader inputs
698
  {
699
    size = 0;
×
700
    for(auto in : input)
×
701
    {
702
      switch(in->type)
×
703
      {
704
        case Types::Empty:
705
          break;
×
706
        case Types::Int:
707
        case Types::Float:
708
          size += 4;
×
709
          break;
×
710
        case Types::Vec2:
711
          size += 8;
×
712
          if(size % 8 != 0)
×
713
            size += 4;
×
714
          break;
×
715
        case Types::Vec3:
716
          while(size % 16 != 0)
×
717
          {
718
            size += 4;
×
719
          }
720
          size += 12;
×
721
          break;
×
722
        case Types::Vec4:
723
          while(size % 16 != 0)
×
724
          {
725
            size += 4;
×
726
          }
727
          size += 16;
×
728
          break;
×
729
        case Types::Image: {
730
          SCORE_TODO;
×
731
          /*
732
          auto sampler = rhi.newSampler(
733
              QRhiSampler::Linear,
734
              QRhiSampler::Linear,
735
              QRhiSampler::None,
736
              QRhiSampler::ClampToEdge,
737
              QRhiSampler::ClampToEdge);
738
          sampler->setName("DefaultShaderMaterial::sampler");
739
          SCORE_ASSERT(sampler->create());
740

741
          samplers.push_back(
742
              {sampler, renderer.textureTargetForInputPort(*in)});
743
*/
744
          break;
×
745
        }
746
        case Types::Audio:
747
          break;
×
748
        case Types::Geometry:
749
          break;
×
750
        case Types::Camera:
751
          size += sizeof(ModelCameraUBO);
×
752
          break;
×
753
      }
754
    }
755

756
    if(size > 0)
×
757
    {
758
      buffer = rhi.newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, size);
×
759
      buffer->setName("DefaultShaderMaterial::buffer");
×
760
      SCORE_ASSERT(buffer->create());
×
761
    }
×
762
  }
763
}
×
764

765
QSize resizeTextureSize(QSize sz, int min, int max) noexcept
×
766
{
767
  if(sz.width() >= min && sz.height() >= min && sz.width() <= max && sz.height() <= max)
×
768
  {
769
    return sz;
×
770
  }
771
  else
772
  {
773
    // To prevent division by zero
774
    if(sz.width() < 1)
×
775
    {
776
      sz.rwidth() = 1;
×
777
    }
×
778
    if(sz.height() < 1)
×
779
    {
780
      sz.rheight() = 1;
×
781
    }
×
782

783
    // Rescale to max dimension by maintaining aspect ratio
784
    if(sz.width() > max && sz.height() > max)
×
785
    {
786
      qreal factor = max / qreal(std::max(sz.width(), sz.height()));
×
787
      sz.rwidth() *= factor;
×
788
      sz.rheight() *= factor;
×
789
    }
×
790
    else if(sz.width() > max)
×
791
    {
792
      qreal factor = (qreal)max / sz.width();
×
793
      sz.rwidth() *= factor;
×
794
      sz.rheight() *= factor;
×
795
    }
×
796
    else if(sz.height() > max)
×
797
    {
798
      qreal factor = (qreal)max / sz.height();
×
799
      sz.rwidth() *= factor;
×
800
      sz.rheight() *= factor;
×
801
    }
×
802

803
    // In case we rescaled below min
804
    if(sz.width() < min)
×
805
    {
806
      sz.rwidth() = min;
×
807
    }
×
808
    if(sz.height() < min)
×
809
    {
810
      sz.rheight() = min;
×
811
    }
×
812
  }
813
  return sz;
×
814
}
×
815

816
QImage resizeTexture(const QImage& img, int min, int max) noexcept
×
817
{
818
  QSize sz = img.size();
×
819
  QSize rescaled = resizeTextureSize(sz, min, max);
×
820
  if(rescaled == sz || sz.width() == 0 || sz.height() == 0)
×
821
    return img;
×
822

823
  return img.scaled(rescaled, Qt::KeepAspectRatio);
×
824
}
×
825

826
QSizeF computeScaleForMeshSizing(ScaleMode mode, QSizeF viewport, QSizeF texture)
×
827
{
828
  if(viewport.isEmpty() || texture.isEmpty())
×
829
    return QSizeF{1., 1.};
×
830
  switch(mode)
×
831
  {
832
    case score::gfx::ScaleMode::BlackBars: {
833
      const auto new_tex_size
834
          = viewport.scaled(texture, Qt::AspectRatioMode::KeepAspectRatioByExpanding);
×
835
      return {
×
836
          texture.width() / new_tex_size.width(),
×
837
          texture.height() / new_tex_size.height()};
×
838
    }
839
    case score::gfx::ScaleMode::Fill: {
840
      double correct_ratio_w = 2. * texture.width() / viewport.width();
×
841
      double correct_ratio_h = 2. * texture.height() / viewport.height();
×
842
      if(texture.width() >= viewport.width() && texture.height() >= viewport.height())
×
843
      {
844
        double rw = viewport.width() / texture.width();
×
845
        double rh = viewport.height() / texture.height();
×
846
        double min = std::max(rw, rh) / 2.;
×
847

848
        return {correct_ratio_w * min, correct_ratio_h * min};
×
849
      }
850
      const auto new_tex_size1
851
          = viewport.scaled(texture, Qt::AspectRatioMode::KeepAspectRatio);
×
852
      return {
×
853
          texture.width() / new_tex_size1.width(),
×
854
          texture.height() / new_tex_size1.height()};
×
855
    }
856
    case score::gfx::ScaleMode::Original: {
857
      return {texture.width() / viewport.width(), texture.height() / viewport.height()};
×
858
    }
859
    case score::gfx::ScaleMode::Stretch:
×
860
    default:
861
      return {1., 1.};
×
862
  }
863
}
×
864

865
QSizeF
866
computeScaleForTexcoordSizing(ScaleMode mode, QSizeF renderSize, QSizeF textureSize)
×
867
{
868
  if(renderSize.isEmpty() || textureSize.isEmpty())
×
869
    return QSizeF{1., 1.};
×
870
  switch(mode)
×
871
  {
872
    // Fits the viewport at the original texture aspect ratio, with black bars around
873
    case score::gfx::ScaleMode::BlackBars: {
874
      const auto textureAspect = textureSize.width() / textureSize.height();
×
875
      const auto renderAspect = renderSize.width() / renderSize.height();
×
876

877
      if(textureAspect > renderAspect)
×
878
        return {1.0, textureAspect / renderAspect};
×
879
      else
880
        return {renderAspect / textureAspect, 1.0};
×
881
    }
882

883
    // Fits the viewport by stretching
884
    case score::gfx::ScaleMode::Stretch:
885
      return {1., 1.};
×
886

887
    // Fits the viewport by filling, maintaining aspect ratio and cropping if necessary
888
    case score::gfx::ScaleMode::Fill: {
889
      const auto textureAspect = textureSize.width() / textureSize.height();
×
890
      const auto renderAspect = renderSize.width() / renderSize.height();
×
891

892
      if(textureAspect > renderAspect)
×
893
        return {renderAspect / textureAspect, 1.0};
×
894
      else
895
        return {1.0, textureAspect / renderAspect};
×
896
    }
897

898
    case score::gfx::ScaleMode::Original: {
899
      return {
×
900
          renderSize.width() / textureSize.width(),
×
901
          renderSize.height() / textureSize.height()};
×
902
    }
903
    default:
904
      return {};
×
905
  }
906
}
×
907

908
std::vector<Sampler> initInputSamplers(
×
909
    const score::gfx::Node& node, RenderList& renderer, const std::vector<Port*>& ports)
910
{
911
  std::vector<Sampler> samplers;
×
912
  QRhi& rhi = *renderer.state.rhi;
×
913

914
  int cur_port = 0;
×
915
  for(Port* in : ports)
×
916
  {
917
    switch(in->type)
×
918
    {
919
      case Types::Image: {
920
        if((in->flags & Flag::GrabsFromSource) == Flag::GrabsFromSource)
×
921
        {
922
          // GrabsFromSource: the upstream node owns the texture (e.g. cubemap).
923
          // We don't create a render target — just grab the texture pointer
924
          // from the source renderer and create a sampler for it.
925
          QRhiTexture* srcTex = nullptr;
×
926

927
          for(auto* edge : in->edges)
×
928
          {
929
            if(auto* src_node = edge->source->node)
×
930
            {
931
              if(auto src_it = src_node->renderedNodes.find(&renderer);
×
932
                 src_it != src_node->renderedNodes.end())
×
933
              {
934
                if(auto* src_renderer = src_it->second)
×
935
                {
936
                  srcTex = src_renderer->textureForOutput(*edge->source);
×
937
                  break;
×
938
                }
939
              }
×
940
            }
×
941
          }
942

943
          if(!srcTex)
×
944
            srcTex = &renderer.emptyTexture();
×
945

946
          auto sampler = rhi.newSampler(
×
947
              QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::Linear,
948
              QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
949
          sampler->setName("initInputSamplers::cubemap_sampler");
×
950
          SCORE_ASSERT(sampler->create());
×
951

952
          samplers.push_back({sampler, srcTex});
×
953
        }
×
954
        else
955
        {
956
          // Look up the pre-created render target from the RenderList
957
          auto rt = renderer.renderTargetForInputPort(*in);
×
958
          auto* texture = rt.texture ? rt.texture : &renderer.emptyTexture();
×
959

960
          auto spec = node.resolveRenderTargetSpecs(cur_port, renderer);
×
961
          auto sampler = rhi.newSampler(
×
962
              spec.mag_filter, spec.min_filter, spec.mipmap_mode, spec.address_u,
×
963
              spec.address_v, spec.address_w);
×
964
          sampler->setName("initInputSamplers::sampler");
×
965
          SCORE_ASSERT(sampler->create());
×
966

967
          samplers.push_back({sampler, texture});
×
968

969
          // If this port has sampleable depth, add depth sampler
970
          if((in->flags & Flag::SamplableDepth) == Flag::SamplableDepth)
×
971
          {
972
            auto depthSampler = rhi.newSampler(
×
973
                QRhiSampler::Nearest, QRhiSampler::Nearest, QRhiSampler::None,
974
                QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
975
            depthSampler->setName("initInputSamplers::depth_sampler");
×
976
            SCORE_ASSERT(depthSampler->create());
×
977

978
            auto* depthTex = rt.depthTexture ? rt.depthTexture : &renderer.emptyTexture();
×
979
            samplers.push_back({depthSampler, depthTex});
×
980
          }
×
981
        }
×
982
        break;
×
983
      }
984

985
      default:
986
        break;
×
987
    }
988
    cur_port++;
×
989
  }
990
  return samplers;
×
991
}
×
992
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc