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

ossia / score / 30144558388

25 Jul 2026 04:44AM UTC coverage: 15.31% (-0.001%) from 15.311%
30144558388

Pull #2147

github

web-flow
Merge 98c48c25e into 13afd939a
Pull Request #2147: gfx: fall back to RGBA8 where the backend has no BGRA8 (fixes images on wasm)

0 of 19 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

30392 of 198510 relevant lines covered (15.31%)

984.1 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/ImageNode.cpp
1
#include <Gfx/Graph/ImageNode.hpp>
2
#include <Gfx/Graph/NodeRenderer.hpp>
3
#include <Gfx/Graph/RenderList.hpp>
4
#include <Gfx/Graph/RenderState.hpp>
5
#include <Gfx/Images/Process.hpp>
6

7
#if QT_SVG_LIB
8
#include <QPainter>
9
#include <QSvgRenderer>
10
#endif
11

12
#include <ossia/detail/math.hpp>
13
#include <ossia/gfx/port_index.hpp>
14
#include <ossia/network/value/value_conversion.hpp>
15

16
namespace score::gfx
17
{
18
static int imageIndex(int idx, int size)
×
19
{
20
  if(size > 0)
×
21
  {
22
    idx %= size;
×
23
    if(idx < 0)
×
24
      idx += size;
×
25

26
    SCORE_ASSERT(idx >= 0);
×
27
    SCORE_ASSERT(idx < size);
×
28

29
    return idx;
×
30
  }
31
  else
32
  {
33
    return 0;
×
34
  }
35
}
×
36

37
static const constexpr auto images_single_vertex_shader = R"_(#version 450
38
layout(location = 0) in vec2 position;
39
layout(location = 1) in vec2 texcoord;
40

41
layout(binding = 3) uniform sampler2D y_tex;
42
layout(location = 0) out vec2 v_texcoord;
43

44
layout(std140, binding = 0) uniform renderer_t {
45
  mat4 clipSpaceCorrMatrix;
46
  vec2 renderSize;
47
} renderer;
48

49
layout(std140, binding = 2) uniform material_t {
50
  int idx;
51
  float opacity;
52
  vec2 position;
53
  vec2 scale;
54
} mat;
55
out gl_PerVertex { vec4 gl_Position; };
56

57
void main()
58
{
59
  v_texcoord = texcoord;
60
  gl_Position = renderer.clipSpaceCorrMatrix * vec4(position * mat.scale + mat.position, 0.0, 1.);
61
#if defined(QSHADER_HLSL) || defined(QSHADER_MSL)
62
  gl_Position.y = - gl_Position.y;
63
#endif
64
}
65
)_";
66

67
static const constexpr auto images_single_fragment_shader = R"_(#version 450
68
layout(std140, binding = 0) uniform renderer_t {
69
  mat4 clipSpaceCorrMatrix;
70
  vec2 renderSize;
71
} renderer;
72

73
layout(std140, binding = 2) uniform material_t {
74
  int idx;
75
  float opacity;
76
  vec2 position;
77
  vec2 scale;
78
} mat;
79

80
layout(binding=3) uniform sampler2D y_tex;
81

82
layout(location = 0) in vec2 v_texcoord;
83
layout(location = 0) out vec4 fragColor;
84

85
void main ()
86
{
87
  fragColor = texture(y_tex, v_texcoord) * mat.opacity;
88
}
89
)_";
90

91
static const constexpr auto images_tiled_vertex_shader = R"_(#version 450
92
layout(location = 0) in vec2 position;
93
layout(location = 1) in vec2 texcoord;
94

95
layout(location = 0) out vec2 v_texcoord;
96

97
layout(std140, binding = 0) uniform renderer_t {
98
  mat4 clipSpaceCorrMatrix;
99
  vec2 renderSize;
100
} renderer;
101

102
layout(std140, binding = 2) uniform material_t {
103
  int idx;
104
  float opacity;
105
  vec2 position;
106
  vec2 scale;
107
} mat;
108

109

110
out gl_PerVertex { vec4 gl_Position; };
111

112
void main()
113
{
114
  v_texcoord = texcoord;
115
  gl_Position = renderer.clipSpaceCorrMatrix * vec4(position.xy, 0.0, 1.);
116
#if defined(QSHADER_HLSL) || defined(QSHADER_MSL)
117
  gl_Position.y = - gl_Position.y;
118
#endif
119
}
120
)_";
121

122
static const constexpr auto images_tiled_fragment_shader = R"_(#version 450
123
layout(std140, binding = 0) uniform renderer_t {
124
  mat4 clipSpaceCorrMatrix;
125
  vec2 renderSize;
126
} renderer;
127

128
layout(std140, binding = 2) uniform material_t {
129
  int idx;
130
  float opacity;
131
  vec2 position;
132
  vec2 scale;
133
} mat;
134

135
layout(binding=3) uniform sampler2D y_tex;
136

137
layout(location = 0) in vec2 v_texcoord;
138
layout(location = 0) out vec4 fragColor;
139

140
void main ()
141
{
142
  fragColor = texture(y_tex, v_texcoord * mat.scale + (1.0f - mat.scale) / 2.0f + vec2(-mat.position.x, mat.position.y)) * mat.opacity;
143
}
144
)_";
145
ImagesNode::ImagesNode(const score::DocumentContext& ctx)
×
146
    : ctx{ctx}
147
{
×
148
  input.push_back(new Port{this, &ubo.currentImageIndex, Types::Int, {}});
×
149
  input.push_back(new Port{this, &ubo.opacity, Types::Float, {}});
×
150
  input.push_back(new Port{this, &ubo.position[0], Types::Vec2, {}});
×
151
  input.push_back(new Port{this, &ubo.scale[0], Types::Vec2, {}});
×
152
  output.push_back(new Port{this, {}, Types::Image, {}});
×
153

154
  m_materialData.reset((char*)&ubo);
×
155
}
×
156

157
void ImagesNode::process(Message&& msg)
×
158
{
159
  ProcessNode::process(msg.token);
×
160

161
  int32_t p = 0;
×
162
  for(auto&& m : std::move(msg.input))
×
163
  {
164
    if(auto val = ossia::get_if<ossia::value>(&m))
×
165
    {
166
      switch(p)
×
167
      {
168
        case 0: // Image index
169
        {
170
          auto sink = ossia::gfx::port_index{msg.node_id, p};
×
171
          ossia::visit(
×
172
              [this, sink](const auto& v) { ProcessNode::process(sink.port, v); },
×
173
              std::move(m));
×
174

175
          break;
×
176
        }
177
        case 1: // Opacity
178
        {
179
          auto opacity = ossia::convert<float>(*val);
×
180
          this->ubo.opacity = ossia::clamp(opacity, 0.f, 1.f);
×
181
          this->materialChange();
×
182
          break;
×
183
        }
184
        case 2: // Position
185
        {
186
          auto sink = ossia::gfx::port_index{msg.node_id, p};
×
187
          ossia::visit(
×
188
              [this, sink](const auto& v) { ProcessNode::process(sink.port, v); },
×
189
              std::move(m));
×
190
          break;
×
191
        }
192

193
        case 3: // X scale
194
        {
195
          auto scale = ossia::convert<float>(*val);
×
196
          this->scale_w = scale;
×
197
          this->materialChange();
×
198
          break;
×
199
        }
200
        case 4: // Scale Y
201
        {
202
          auto scale = ossia::convert<float>(*val);
×
203
          this->scale_h = scale;
×
204
          this->materialChange();
×
205
          break;
×
206
        }
207

208
        case 5: // Images
209
        {
210
          auto new_images = Gfx::getImages(*val, this->ctx);
×
211
          auto diff = [](const score::gfx::Image& lhs, const score::gfx::Image& rhs) {
×
212
            return lhs.path != rhs.path;
×
213
          };
214
          bool images_changed = new_images.size() != images.size();
×
215
          if(!images_changed)
×
216
          {
217
            for(int i = 0; i < images.size(); i++)
×
218
              images_changed |= diff(images[i], new_images[i]);
×
219
          }
×
220

221
          if(images_changed)
×
222
          {
223
            clear();
×
224
            images = std::move(new_images);
×
225
            for(auto& img : images)
×
226
            {
227
#if Q_SVG_LIB
228
              if(img.path.endsWith("svg"))
229
              {
230
                auto renderer = new QSvgRenderer{img.path};
231
                if(renderer->animated())
232
                  renderer->setAnimationEnabled(true);
233
                renderer->setFramesPerSecond(60);
234
                linearImages.push_back(renderer);
235
              }
236
              else
237
#endif
238
              {
239
                for(auto& frame : img.frames)
×
240
                {
241
                  linearImages.push_back(&frame);
×
242
                }
243
              }
244
            }
245

246
            ++this->imagesChanged;
×
247
          }
×
248
          break;
249
        }
×
250

251
        case 6: // Tile
252
        {
253
          this->tileMode = (ImageMode)ossia::convert<int>(*val);
×
254
          this->materialChange();
×
255
          break;
×
256
        }
257

258
        case 7: // Scale
259
        {
260
          this->scaleMode = (ScaleMode)ossia::convert<int>(*val);
×
261
          this->materialChange();
×
262
          break;
×
263
        }
264
      }
265
    }
×
266

267
    p++;
×
268
  }
269
}
×
270

271
void ImagesNode::clear()
×
272
{
273
#if Q_SVG_LIB
274
  for(auto image : linearImages)
275
  {
276
    if(auto svg = std::get_if<QSvgRenderer*>(&image))
277
    {
278
      delete *svg;
279
    }
280
  }
281
#endif
282

283
  linearImages.clear();
×
284
  Gfx::releaseImages(images);
×
285
}
×
286

287
ImagesNode::~ImagesNode()
×
288
{
×
289
  clear();
×
290
  m_materialData.release();
×
291
}
×
292

293
static QRhiSampler* createSampler(ImageMode tile, QRhi& rhi)
×
294
{
295
  QRhiSampler::AddressMode am{};
×
296
  switch(tile)
×
297
  {
×
298
    default:
299
    case Single:
300
      am = QRhiSampler::ClampToEdge;
×
301
      break;
×
302
    case Clamped:
303
      am = QRhiSampler::ClampToEdge;
×
304
      break;
×
305
    case Tiled:
306
      am = QRhiSampler::Repeat;
×
307
      break;
×
308
    case Mirrored:
309
      am = QRhiSampler::Mirror;
×
310
      break;
×
311
  }
312

313
  auto sampler = rhi.newSampler(
×
314
      QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None, am, am);
×
315

316
  sampler->setName("ImagesNode::sampler");
×
317
  sampler->create();
×
318
  return sampler;
×
319
}
×
320

321
class ImagesNode::PreloadedRenderer : public GenericNodeRenderer
322
{
323
public:
324
  using GenericNodeRenderer::GenericNodeRenderer;
×
325

326
private:
327
  ~PreloadedRenderer() { }
×
328

329
  int imagesChanged = -1;
×
330
  ImageMode tile{};
×
331
  ScaleMode scale{score::gfx::ScaleMode::Original};
×
332
  QSizeF lastRenderSize;
333
  bool mustRecomputeSize{true};
×
334
  float scale_w{1.f};
×
335
  float scale_h{1.f};
×
336

337
  void recreateTextures(QRhi& rhi)
×
338
  {
339
    auto& n = static_cast<const ImagesNode&>(this->node);
×
340

341
    const int limits_min = rhi.resourceLimit(QRhi::ResourceLimit::TextureSizeMin);
×
342
    const int limits_max = rhi.resourceLimit(QRhi::ResourceLimit::TextureSizeMax);
×
343

344
    for(int i = 0, N = n.linearImages.size(); i < N; i++)
×
345
    {
346
      auto frame = n.linearImages[i];
×
347
      QSize sz{limits_min, limits_min};
×
348
      if(auto qimage = std::get_if<QImage*>(&frame))
×
349
      {
350
        sz = (*qimage)->size();
×
351
      }
×
352
#if Q_SVG_LIB
353
      else if(auto svg_p = std::get_if<QSvgRenderer*>(&frame))
354
      {
355
        auto* svg = *svg_p;
356
        auto svg_size = svg->defaultSize();
357
        svg_size = QSize(
358
            svg_size.width() * std::abs(scale_w), svg_size.height() * std::abs(scale_h));
359
        sz = svg_size;
360
      }
361
#endif
362
      const auto tex_size
363
          = resizeTextureSize(QSize{sz.width(), sz.height()}, limits_min, limits_max);
×
364

365
      if(m_textures.size() <= i)
×
366
      {
367
        QRhiTexture* tex = tex
×
NEW
368
            = rhi.newTexture(imageTextureFormat(rhi), tex_size, 1, QRhiTexture::Flag{});
×
369
        tex->setName("ImagesNode::tex");
×
370
        tex->create();
×
371
        m_textures.push_back(tex);
×
372
      }
×
373
      else if(m_textures[i]->pixelSize() != tex_size)
×
374
      {
375
        auto tex = m_textures[i];
×
376
        tex->destroy();
×
377
        tex->setPixelSize(tex_size);
×
378
        tex->create();
×
379
      }
×
380
    }
×
381
  }
×
382

383
  TextureRenderTarget renderTargetForInput(const Port& p) override { return {}; }
×
384
  void init(RenderList& renderer, QRhiResourceUpdateBatch& res) override
×
385
  {
386
    auto& n = static_cast<const ImagesNode&>(this->node);
×
387
    const auto& rs = renderer.state;
×
388
    const Mesh& mesh = renderer.defaultQuad();
×
389

390
    defaultMeshInit(renderer, mesh, res);
×
391
    processUBOInit(renderer);
×
392
    m_material.init(renderer, node.input, m_samplers);
×
393

394
    m_ubo.currentImageIndex = -1;
×
395
    QRhi& rhi = *renderer.state.rhi;
×
396

397
    // Create GPU textures for each image
398
    recreateTextures(rhi);
×
399

400
    tile = n.tileMode;
×
401
    std::tie(m_vertexS, m_fragmentS) = score::gfx::makeShaders(
×
402
        rs, images_single_vertex_shader, images_single_fragment_shader);
×
403

404
    // Create the sampler in which we are going to put the texture
405
    {
406
      auto sampler = createSampler(tile, rhi);
×
407
      auto tex = m_textures.empty() ? &renderer.emptyTexture() : m_textures.front();
×
408
      m_samplers.push_back({sampler, tex});
×
409
    }
410

411
    // Initialize the passes for the "single" case
412
    defaultPassesInit(renderer, mesh);
×
413

414
    // Initialize the passes for the "tiled" case
415
    {
416
      auto [v, f] = score::gfx::makeShaders(
×
417
          rs, images_tiled_vertex_shader, images_tiled_fragment_shader);
×
418
      for(Edge* edge : this->node.output[0]->edges)
×
419
      {
420
        auto rt = renderer.renderTargetForOutput(*edge);
×
421
        if(rt.renderTarget)
×
422
        {
423
          m_altPasses.emplace_back(
×
424
              edge, score::gfx::buildPipeline(
×
425
                        renderer, mesh, v, f, rt, m_processUBO, m_material.buffer,
×
426
                        m_samplers));
×
427
        }
×
428
      }
×
429
    }
×
430
  }
×
431

432
  void update(RenderList& renderer, QRhiResourceUpdateBatch& res, Edge* edge) override
×
433
  {
434
    auto& n = (static_cast<const ImagesNode&>(this->node));
×
435
    if(n.tileMode != tile)
×
436
    {
437
      tile = n.tileMode;
×
438
      auto [s, tex] = m_samplers[0];
×
439
      m_samplers.clear();
×
440

441
      // Create a new sampler
442
      auto new_sampler = createSampler(tile, *renderer.state.rhi);
×
443
      m_samplers.push_back({new_sampler, tex});
×
444

445
      // Replace it in the render passes
446
      auto replace_sampler = [](PassMap& passes, QRhiSampler* oldS, QRhiSampler* newS) {
×
447
        for(auto& pass : passes)
×
448
          score::gfx::replaceSampler(*pass.second.srb, oldS, newS);
×
449
      };
×
450

451
      replace_sampler(m_p, s, new_sampler);
×
452
      replace_sampler(m_altPasses, s, new_sampler);
×
453

454
      // Release the old sampler
455
      mustRecomputeSize = true;
×
456
      imagesChanged = -1;
×
457
      s->deleteLater();
×
458
    }
×
459

460
    bool updateCurrentTexture = false;
×
461
    if(n.imagesChanged > imagesChanged)
×
462
    {
463
      imagesChanged = n.imagesChanged;
×
464
      if(m_textures.size() > n.linearImages.size())
×
465
      {
466
        for(int i = n.linearImages.size(); i < m_textures.size(); i++)
×
467
        {
468
          m_textures[i]->deleteLater();
×
469
        }
×
470
        m_textures.resize(n.linearImages.size());
×
471
      }
×
472

473
      m_uploaded = false;
×
474
    }
×
475

476
    recreateTextures(*renderer.state.rhi);
×
477

478
    // If images haven't been uploaded yet, upload them.
479
    {
480
      static thread_local QImage temp_svg;
×
481

482
      std::size_t k = 0;
×
483
      for(auto frame : n.linearImages)
×
484
      {
485
        if(auto qimage = std::get_if<QImage*>(&frame))
×
486
        {
487
          if(!m_uploaded)
×
488
          {
489
            res.uploadTexture(m_textures[k], renderer.adaptImage(**qimage));
×
490
            updateCurrentTexture = true;
×
491
          }
×
492
        }
×
493
#if Q_SVG_LIB
494
        else if(auto svg = std::get_if<QSvgRenderer*>(&frame))
495
        {
496
          auto svg_size = (*svg)->defaultSize();
497
          svg_size = QSize(
498
              svg_size.width() * std::abs(scale_w),
499
              svg_size.height() * std::abs(scale_h));
500
          if(!svg_size.isEmpty())
501
          {
502
            if((*svg)->animated())
503
            {
504
              if(temp_svg.size() != svg_size)
505
                temp_svg = QImage(svg_size, QImage::Format_ARGB32);
506
              QPainter temp_svg_painter{&temp_svg};
507
              temp_svg.fill(0);
508
              (*svg)->render(&temp_svg_painter);
509
              res.uploadTexture(m_textures[k], renderer.adaptImage(temp_svg));
510
              updateCurrentTexture = true;
511
            }
512
            else
513
            {
514
              if(!m_uploaded)
515
              {
516
                if(temp_svg.size() != svg_size)
517
                  temp_svg = QImage(svg_size, QImage::Format_ARGB32);
518
                QPainter temp_svg_painter{&temp_svg};
519
                temp_svg.fill(0);
520
                (*svg)->render(&temp_svg_painter);
521
                res.uploadTexture(m_textures[k], renderer.adaptImage(temp_svg));
522
                updateCurrentTexture = true;
523
              }
524
            }
525
          }
526
        }
527
#endif
528

529
        k++;
×
530
      }
531
      m_uploaded = true;
×
532
    }
533

534
    // If the current image being displayed by this renderer (in m_prev_ubo)
535
    // is out of date with the image in the data model, we switch the texture
536
    int currentImageIndex = -1;
×
537
    if(updateCurrentTexture || m_ubo.currentImageIndex != n.ubo.currentImageIndex)
×
538
    {
539
      auto replace_texture
×
540
          = [](PassMap& passes, QRhiSampler* sampler, QRhiTexture* tex) {
×
541
        for(auto& pass : passes)
×
542
          score::gfx::replaceTexture(*pass.second.srb, sampler, tex);
×
543
      };
×
544

545
      currentImageIndex = imageIndex(n.ubo.currentImageIndex, m_textures.size());
×
546
      QRhiSampler* sampler = m_samplers[0].sampler;
×
547
      QRhiTexture* new_tex{};
×
548
      if(ossia::valid_index(currentImageIndex, m_textures))
×
549
      {
550
        new_tex = m_textures[currentImageIndex];
×
551
      }
×
552
      else
553
      {
554
        new_tex = &renderer.emptyTexture();
×
555
      }
556

557
      replace_texture(m_p, sampler, new_tex);
×
558
      replace_texture(m_altPasses, sampler, new_tex);
×
559

560
      m_ubo.currentImageIndex = n.ubo.currentImageIndex;
×
561
      mustRecomputeSize = true;
×
562
    }
×
563

564
    // Copy the model UBO into the renderer
565
    m_ubo = n.ubo;
×
566

567
    if(edge)
×
568
    {
569
      const QSizeF renderSize = renderer.renderSize(edge);
×
570
      if(mustRecomputeSize || lastRenderSize != renderSize || scale != n.scaleMode
×
571
         || scale_w != n.scale_w || scale_h != n.scale_h || materialChanged)
×
572
      {
573
        scale = n.scaleMode;
×
574
        lastRenderSize = renderSize;
×
575
        scale_w = n.scale_w;
×
576
        scale_h = n.scale_h;
×
577

578
        QSizeF textureSize{1, 1};
×
579

580
        if(currentImageIndex == -1)
×
581
          currentImageIndex = imageIndex(m_ubo.currentImageIndex, m_textures.size());
×
582
        if(currentImageIndex < std::ssize(m_textures))
×
583
          textureSize = m_textures[currentImageIndex]->pixelSize();
×
584

585
        if(currentImageIndex < n.linearImages.size())
×
586
        {
587
#if Q_SVG_LIB
588
          const bool is_svg = n.linearImages[currentImageIndex].index() == 1;
589
          if(is_svg)
590
          {
591
            if(tile == score::gfx::Single)
592
            {
593
              auto sz = computeScaleForMeshSizing(scale, renderSize, textureSize);
594
              m_ubo.scale[0] = sz.width();
595
              m_ubo.scale[1] = sz.height();
596
            }
597
            else
598
            {
599
              auto sz = computeScaleForTexcoordSizing(scale, renderSize, textureSize);
600
              m_ubo.scale[0] = sz.width();
601
              m_ubo.scale[1] = sz.height();
602
            }
603
          }
604
          else
605
#endif
606
          {
607
            if(tile == score::gfx::Single)
×
608
            {
609
              auto sz = computeScaleForMeshSizing(scale, renderSize, textureSize);
×
610
              m_ubo.scale[0] = sz.width() * scale_w;
×
611
              m_ubo.scale[1] = sz.height() * scale_h;
×
612
            }
×
613
            else
614
            {
615
              auto sz = computeScaleForTexcoordSizing(scale, renderSize, textureSize);
×
616
              m_ubo.scale[0] = sz.width() / scale_w;
×
617
              m_ubo.scale[1] = sz.height() / scale_h;
×
618
            }
619
          }
620
        }
×
621

622
        materialChanged = true;
×
623
        mustRecomputeSize = false;
×
624
      }
×
625
    }
×
626

627
    // We can't use generic update since we need some modifications on the UBO
628
    // depending on the output resolution, so each renderer needs its own UBO:
629
    // GenericNodeRenderer::update(renderer, res, edge);
630

631
    defaultMeshUpdate(renderer, res);
×
632

633
    // FIXME check if it is actually used by the shaders
634
    res.updateDynamicBuffer(m_processUBO, 0, sizeof(ProcessUBO), &n.standardUBO);
×
635

636
    if(m_material.buffer && m_material.size > 0)
×
637
    {
638
      if(materialChanged)
×
639
      {
640
        res.updateDynamicBuffer(m_material.buffer, 0, m_material.size, &m_ubo);
×
641
      }
×
642
    }
×
643
  }
×
644

645
  void runRenderPass(RenderList& renderer, QRhiCommandBuffer& cb, Edge& edge) override
×
646
  {
647
    const auto& mesh = renderer.defaultQuad();
×
648
    if(tile == ImageMode::Single)
×
649
      defaultRenderPass(renderer, mesh, cb, edge, m_p);
×
650
    else
651
      defaultRenderPass(renderer, mesh, cb, edge, m_altPasses);
×
652
  }
×
653

654
  void release(RenderList& r) override
×
655
  {
656
    for(auto tex : m_textures)
×
657
    {
658
      tex->deleteLater();
×
659
    }
660
    m_textures.clear();
×
661

662
    defaultRelease(r);
×
663

664
    {
665
      for(auto& pass : m_altPasses)
×
666
        pass.second.release();
×
667
      m_altPasses.clear();
×
668
    }
669
  }
×
670

671
  struct ImagesNode::UBO m_ubo;
672
  ossia::small_vector<std::pair<Edge*, Pipeline>, 2> m_altPasses;
673
  std::vector<QRhiTexture*> m_textures;
674
  bool m_uploaded = false;
×
675
};
676

677
#if 0
678
class ImagesNode::OnTheFlyRenderer : public GenericNodeRenderer
679
{
680
public:
681
  using GenericNodeRenderer::GenericNodeRenderer;
682

683
private:
684
  ~OnTheFlyRenderer() { }
685

686
  int imagesChanged = -1;
687
  ImageMode tile{};
688

689
  void recreateTexture(QRhi& rhi)
690
  {
691
    auto& n = static_cast<const ImagesNode&>(this->node);
692

693
    const int limits_min = rhi.resourceLimit(QRhi::ResourceLimit::TextureSizeMin);
694
    const int limits_max = rhi.resourceLimit(QRhi::ResourceLimit::TextureSizeMax);
695

696
    QSize maxSize{1, 1};
697
    for(const auto frame : n.linearImages)
698
    {
699
      const auto sz = resizeTextureSize(frame->size(), limits_min, limits_max);
700

701
      maxSize.setWidth(std::max(maxSize.width(), sz.width()));
702
      maxSize.setHeight(std::max(maxSize.height(), sz.height()));
703
    }
704

705
    auto tex = rhi.newTexture(imageTextureFormat(rhi), maxSize, 1, QRhiTexture::Flag{});
706

707
    tex->setName("OnTheFlyRenderer::tex");
708
    tex->create();
709

710
    m_texture = tex;
711
  }
712

713
  TextureRenderTarget renderTargetForInput(const Port& p) override { return {}; }
714
  void init(RenderList& renderer, QRhiResourceUpdateBatch& res) override
715
  {
716
    auto& n = static_cast<const ImagesNode&>(this->node);
717
    const auto& rs = renderer.state;
718
    const auto& mesh = renderer.defaultQuad();
719
    defaultMeshInit(renderer, mesh, res);
720
    processUBOInit(renderer);
721
    m_material.init(renderer, node.input, m_samplers);
722

723
    m_prev_ubo.currentImageIndex = -1;
724
    QRhi& rhi = *renderer.state.rhi;
725

726
    tile = n.tileMode;
727
    QShader &v = m_vertexS, &f = m_fragmentS;
728
    if(!tile)
729
      std::tie(v, f) = score::gfx::makeShaders(
730
          rs, images_single_vertex_shader, images_single_fragment_shader);
731
    else
732
      std::tie(v, f) = score::gfx::makeShaders(
733
          rs, images_tiled_vertex_shader, images_tiled_fragment_shader);
734

735
    // Create the sampler in which we are going to put the texture
736
    {
737
      auto sampler = createSampler(tile, rhi);
738

739
      // Create GPU texture
740
      recreateTexture(rhi);
741

742
      m_samplers.push_back({sampler, m_texture});
743
    }
744

745
    // Initialize the passes for the "single" case
746
    defaultPassesInit(renderer, mesh);
747

748
    // Initialize the passes for the "tiled" case
749
    {
750
      auto [v, f] = score::gfx::makeShaders(
751
          rs, images_tiled_vertex_shader, images_tiled_fragment_shader);
752
      for(Edge* edge : this->node.output[0]->edges)
753
      {
754
        auto rt = renderer.renderTargetForOutput(*edge);
755
        if(rt.renderTarget)
756
        {
757
          m_altPasses.emplace_back(
758
              edge, score::gfx::buildPipeline(
759
                        renderer, mesh, v, f, rt, m_processUBO, m_material.buffer,
760
                        m_samplers));
761
        }
762
      }
763
    }
764
  }
765

766
  void update(RenderList& renderer, QRhiResourceUpdateBatch& res, Edge* edge) override
767
  {
768
    auto& n = static_cast<const ImagesNode&>(this->node);
769

770
    if(n.tileMode != tile)
771
    {
772
      tile = n.tileMode;
773
      auto [s, tex] = m_samplers[0];
774

775
      m_samplers.clear();
776

777
      // Create a new sampler
778
      auto new_sampler = createSampler(tile, *renderer.state.rhi);
779
      m_samplers.push_back({new_sampler, tex});
780

781
      // Replace it in the render passes
782
      auto replace_sampler = [](PassMap& passes, QRhiSampler* oldS, QRhiSampler* newS) {
783
        for(auto& pass : passes)
784
          score::gfx::replaceSampler(*pass.second.srb, oldS, newS);
785
      };
786

787
      replace_sampler(m_p, s, new_sampler);
788
      replace_sampler(m_altPasses, s, new_sampler);
789

790
      // Release the old sampler
791
      s->deleteLater();
792
    }
793

794
    if(n.imagesChanged > imagesChanged)
795
    {
796
      imagesChanged = n.imagesChanged;
797
      m_texture->deleteLater();
798
      m_texture = nullptr;
799

800
      recreateTexture(*renderer.state.rhi);
801
      m_uploaded = false;
802

803
      auto replace_texture
804
          = [](PassMap& passes, QRhiSampler* sampler, QRhiTexture* tex) {
805
        for(auto& pass : passes)
806
          score::gfx::replaceTexture(*pass.second.srb, sampler, tex);
807
      };
808

809
      auto sampler = m_samplers[0].sampler;
810
      replace_texture(m_p, sampler, m_texture);
811
      replace_texture(m_altPasses, sampler, m_texture);
812
    }
813

814
    // If the current image being displayed by this renderer (in m_prev_ubo)
815
    // is out of date with the image in the data model, we switch the texture
816
    if(!m_uploaded || m_prev_ubo.currentImageIndex != n.ubo.currentImageIndex)
817
    {
818
      const int idx = imageIndex(n.ubo.currentImageIndex, n.linearImages.size());
819
      if(ossia::valid_index(idx, n.linearImages))
820
      {
821
        auto frame = n.linearImages[idx];
822

823
        res.uploadTexture(m_texture, renderer.adaptImage(*frame));
824

825
        m_prev_ubo.currentImageIndex = n.ubo.currentImageIndex;
826
        m_uploaded = true;
827
      }
828
    }
829

830
    GenericNodeRenderer::update(renderer, res, edge);
831
  }
832

833
  void runRenderPass(RenderList& renderer, QRhiCommandBuffer& cb, Edge& edge) override
834
  {
835
    const auto& mesh = renderer.defaultQuad();
836
    if(tile == ImageMode::Single)
837
      defaultRenderPass(renderer, mesh, cb, edge, m_p);
838
    else
839
      defaultRenderPass(renderer, mesh, cb, edge, m_altPasses);
840
  }
841

842
  void release(RenderList& r) override
843
  {
844
    m_texture->deleteLater();
845
    m_texture = nullptr;
846

847
    defaultRelease(r);
848

849
    {
850
      for(auto& pass : m_altPasses)
851
        pass.second.release();
852
      m_altPasses.clear();
853
    }
854
  }
855

856
  struct ImagesNode::UBO m_prev_ubo;
857
  ossia::small_vector<std::pair<Edge*, Pipeline>, 2> m_altPasses;
858
  QRhiTexture* m_texture{};
859
  bool m_uploaded = false;
860
};
861
#endif
862

863
NodeRenderer* ImagesNode::createRenderer(RenderList& r) const noexcept
×
864
{
865
  return new PreloadedRenderer{*this};
×
866
}
867

868
}
869

870
namespace score::gfx
871
{
872

873
static const constexpr auto fullscreen_images_vertex_shader = R"_(#version 450
874
layout(location = 0) in vec2 position;
875
layout(location = 1) in vec2 texcoord;
876

877
layout(binding = 3) uniform sampler2D y_tex;
878
layout(location = 0) out vec2 v_texcoord;
879

880
layout(std140, binding = 0) uniform renderer_t {
881
  mat4 clipSpaceCorrMatrix;
882
  vec2 renderSize;
883
} renderer;
884

885
out gl_PerVertex { vec4 gl_Position; };
886

887
void main()
888
{
889
  v_texcoord = vec2(texcoord.x, 1. - texcoord.y);
890
  gl_Position = renderer.clipSpaceCorrMatrix * vec4(position.xy, 0.0, 1.);
891
#if defined(QSHADER_HLSL) || defined(QSHADER_MSL)
892
  gl_Position.y = - gl_Position.y;
893
#endif
894
}
895
)_";
896

897
static const constexpr auto fullscreen_images_fragment_shader = R"_(#version 450
898
layout(std140, binding = 0) uniform renderer_t {
899
  mat4 clipSpaceCorrMatrix;
900
  vec2 renderSize;
901
} renderer;
902

903
layout(binding=3) uniform sampler2D y_tex;
904

905
layout(location = 0) in vec2 v_texcoord;
906
layout(location = 0) out vec4 fragColor;
907

908
void main ()
909
{
910
  vec2 factor = textureSize(y_tex, 0) / renderer.renderSize;
911
  vec2 ifactor = renderer.renderSize / textureSize(y_tex, 0);
912
  fragColor = texture(y_tex, v_texcoord);
913
}
914
)_";
915
FullScreenImageNode::FullScreenImageNode(QImage dec)
×
916
    : m_image{std::move(dec)}
×
917
{
×
918
  output.push_back(new Port{this, {}, Types::Image, {}});
×
919
}
×
920

921
FullScreenImageNode::~FullScreenImageNode() { }
×
922

923
class FullScreenImageNode::Renderer : public GenericNodeRenderer
924
{
925
public:
926
  using GenericNodeRenderer::GenericNodeRenderer;
×
927

928
private:
929
  ~Renderer() { }
×
930

931
  TextureRenderTarget renderTargetForInput(const Port& p) override { return {}; }
×
932
  void init(RenderList& renderer, QRhiResourceUpdateBatch& res) override
×
933
  {
934
    const auto& mesh = renderer.defaultTriangle();
×
935
    defaultMeshInit(renderer, mesh, res);
×
936
    processUBOInit(renderer);
×
937
    m_material.init(renderer, node.input, m_samplers);
×
938
    std::tie(m_vertexS, m_fragmentS) = score::gfx::makeShaders(
×
939
        renderer.state, fullscreen_images_vertex_shader,
×
940
        fullscreen_images_fragment_shader);
×
941

942
    auto& n = static_cast<const FullScreenImageNode&>(this->node);
×
943
    auto& rhi = *renderer.state.rhi;
×
944

945
    // Create GPU textures for the image
946
    const QSize sz = n.m_image.size();
×
947
    auto tex = rhi.newTexture(
×
NEW
948
        imageTextureFormat(rhi), QSize{sz.width(), sz.height()}, 1, QRhiTexture::Flag{});
×
949

950
    tex->setName("FullScreenImageNode::tex");
×
951
    tex->create();
×
952
    m_texture = tex;
×
953

954
    // Create the sampler in which we are going to put the texture
955
    {
956
      auto sampler = rhi.newSampler(
×
957
          QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
958
          QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
959

960
      sampler->setName("FullScreenImageNode::sampler");
×
961
      sampler->create();
×
962
      m_samplers.push_back({sampler, m_texture});
×
963
    }
964

965
    defaultPassesInit(renderer, mesh);
×
966
  }
×
967

968
  void update(RenderList& renderer, QRhiResourceUpdateBatch& res, score::gfx::Edge* edge)
×
969
      override
970
  {
971
    GenericNodeRenderer::update(renderer, res, edge);
×
972

973
    auto& n = static_cast<const FullScreenImageNode&>(this->node);
×
974
    // If images haven't been uploaded yet, upload them.
975
    if(!m_uploaded)
×
976
    {
977
      res.uploadTexture(m_texture, renderer.adaptImage(n.m_image));
×
978
      m_uploaded = true;
×
979
    }
×
980
  }
×
981

982
  void runRenderPass(RenderList& renderer, QRhiCommandBuffer& cb, Edge& edge) override
×
983
  {
984
    const auto& mesh = renderer.defaultTriangle();
×
985
    defaultRenderPass(renderer, mesh, cb, edge);
×
986
  }
×
987

988
  void release(RenderList& r) override
×
989
  {
990
    m_texture->deleteLater();
×
991
    m_texture = nullptr;
×
992

993
    defaultRelease(r);
×
994
  }
×
995

996
  QRhiTexture* m_texture{};
×
997
  bool m_uploaded = false;
×
998
};
999

1000
NodeRenderer* FullScreenImageNode::createRenderer(RenderList& r) const noexcept
×
1001
{
1002
  return new Renderer{*this};
×
1003
}
1004

1005
}
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