• 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

3.46
/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.cpp
1
#include "ISFNode.hpp"
2

3
#include <Gfx/Graph/Graph.hpp>
4
#include <Gfx/Graph/NodeRenderer.hpp>
5
#include <Gfx/Graph/OutputNode.hpp>
6
#include <Gfx/Graph/RenderList.hpp>
7
#include <Gfx/Graph/Utils.hpp>
8
#include <Gfx/Graph/Window.hpp>
9

10
#include <score/gfx/Vulkan.hpp>
11
#include <score/tools/Debug.hpp>
12

13
#include <ossia/detail/flat_set.hpp>
14
#include <ossia/detail/hash_map.hpp>
15
#include <ossia/detail/ssize.hpp>
16

17
#include <boost/graph/adjacency_list.hpp>
18
#include <boost/graph/filtered_graph.hpp>
19
#include <boost/graph/topological_sort.hpp>
20

21
namespace score::gfx
22
{
23
template <typename Graph_T, typename IO>
24
void print_graph(Graph_T& g, IO& stream)
25
{
26
#if 0
27
  std::stringstream s;
28
  boost::write_graphviz(
29
      s, g,
30
      [&](auto& out, auto v) {
31
    if(g[v])
32
    {
33

34
      out << "[label=\"";
35
      auto n = g[v];
36
      if(auto i = dynamic_cast<ISFNode*>(n))
37
        out << i->m_descriptor.description;
38
      else
39
        out << "output";
40
      out << "\"]";
41
    }
42
    else
43
      out << "[]";
44
      },
45
      [](auto&&...) {});
46

47
  stream << s.str() << "\n";
48
#endif
49
}
50

51
using Vertex = score::gfx::Node*;
52
using GraphImpl = boost::adjacency_list<
53
    boost::vecS, boost::vecS, boost::directedS, Vertex, Process::CableType>;
54
using VertexMap = ossia::hash_map<score::gfx::Node*, GraphImpl::vertex_descriptor>;
55

56
struct no_delay_edges
×
57
{
58
  const GraphImpl* g{};
×
59

60
  bool operator()(const boost::graph_traits<GraphImpl>::edge_descriptor& e) const
×
61
  {
62
    switch((*g)[e])
×
63
    {
64
      case Process::CableType::ImmediateGlutton:
65
      case Process::CableType::ImmediateStrict:
66
        return true;
×
67
      default:
68
        return false;
×
69
    }
70
  }
×
71
};
72

73
static void graphwalk(
×
74
    score::gfx::Node* node, std::vector<score::gfx::Node*>& list, GraphImpl& g,
75
    VertexMap& m)
76
{
77
  auto sink_desc = m[node];
×
78
  for(auto inputs : node->input)
×
79
  {
80
    for(auto edge : inputs->edges)
×
81
    {
82
      if(!edge->source->node->addedToGraph)
×
83
      {
84
        list.push_back(edge->source->node);
×
85

86
        auto src_desc = boost::add_vertex(edge->source->node, g);
×
87
        m[edge->source->node] = src_desc;
×
88
        edge->source->node->addedToGraph = true;
×
89
        boost::add_edge(src_desc, sink_desc, edge->type, g);
×
90
      }
×
91
      else
92
      {
93
        auto src_desc = m[edge->source->node];
×
94
        boost::add_edge(src_desc, sink_desc, edge->type, g);
×
95
      }
96
    }
97
  }
98
}
×
99

100
static void graphwalk(std::vector<score::gfx::Node*>& model_nodes)
×
101
{
102
  GraphImpl g;
×
103
  VertexMap m;
×
104
  auto k = boost::add_vertex(model_nodes.front(), g);
×
105
  m[model_nodes.front()] = k;
×
106
  model_nodes.front()->addedToGraph = true;
×
107

108
  std::size_t processed = 0;
×
109
  while(processed != model_nodes.size())
×
110
  {
111
    graphwalk(model_nodes[processed], model_nodes, g, m);
×
112
    processed++;
×
113
  }
114

115
  ossia::int_vector topo_order;
×
116
  topo_order.reserve(model_nodes.size());
×
117

118
  try
119
  {
120
    model_nodes.clear();
×
121
    auto view = boost::filtered_graph(g, no_delay_edges{&g});
×
122
    boost::topological_sort(view, std::back_inserter(topo_order));
×
123
    for(auto it = topo_order.begin(); it != topo_order.end(); ++it)
×
124
    {
125
      auto e = *it;
×
126
      SCORE_ASSERT(g[e]);
×
127
      model_nodes.push_back(g[e]);
×
128
    }
×
129
  }
×
130
  catch(const std::exception& e)
131
  {
132
    qDebug() << "Invalid gfx graph: " << e.what();
×
133
  }
×
134
}
×
135

136
void Graph::createAllRenderLists(GraphicsApi graphicsApi)
×
137
{
138
#if QT_HAS_VULKAN
139
  if(graphicsApi == Vulkan)
×
140
  {
141
    if(!staticVulkanInstance())
×
142
    {
143
      qWarning("Failed to create Vulkan instance, switching to OpenGL");
×
144
      graphicsApi = OpenGL;
×
145
    }
×
146
  }
×
147
#endif
148

149
  for(auto output : m_outputs)
×
150
  {
151
    output->stopRendering();
×
152
  }
153

154
  for(auto node : m_nodes)
×
155
  {
156
    node->renderedNodes.clear();
×
157
    node->renderedNodesChanged();
×
158
  }
159

160
  for(auto& renderer : m_renderers)
×
161
  {
162
    renderer->release();
×
163
  }
164

165
  m_renderers.clear();
×
166
  m_outputs.clear();
×
167

168
  ossia::flat_set<OutputNode*> parent_nodes;
×
169
  for(auto node : m_nodes)
×
170
  {
171
    if(auto out = dynamic_cast<OutputNode*>(node))
×
172
    {
173
      m_outputs.push_back(out);
×
174
      if(auto ptr = out->configuration().parent)
×
175
        parent_nodes.insert(ptr);
×
176
    }
×
177
  }
178

179
  // For multi-viewport renders
180
  for(auto node : parent_nodes)
×
181
  {
182
    if(!ossia::contains(m_outputs, node))
×
183
    {
184
      m_outputs.push_back(node);
×
185
    }
×
186
  }
187

188
  m_renderers.reserve(ossia::max(16, std::ssize(m_outputs)));
×
189

190
  for(auto output : m_outputs)
×
191
  {
192
    initializeOutput(output, graphicsApi);
×
193
    output->startRendering();
×
194
  }
195
}
×
196

197
void Graph::createSingleRenderList(
×
198
    score::gfx::OutputNode& output, GraphicsApi graphicsApi)
199
{
200
#if QT_HAS_VULKAN
201
  if(graphicsApi == Vulkan)
×
202
  {
203
    if(!staticVulkanInstance())
×
204
    {
205
      qWarning("Failed to create Vulkan instance, switching to OpenGL");
×
206
      graphicsApi = OpenGL;
×
207
    }
×
208
  }
×
209
#endif
210

211
  initializeOutput(&output, graphicsApi);
×
212
  output.startRendering();
×
213
}
×
214

215
void Graph::createOutputRenderList(OutputNode& output)
×
216
try
217
{
218
  if(output.renderState())
×
219
  {
220
    if(auto rl = createRenderList(&output, output.renderState()))
×
221
      m_renderers.push_back(std::move(rl));
×
222
  }
×
223
}
×
224
catch(...)
225
{
226
}
×
227

228
void Graph::recreateOutputRenderList(OutputNode& output)
×
229
{
230
  auto it = ossia::find_if(
×
231
      m_renderers, [rend = output.renderer()](const std::shared_ptr<RenderList>& r) {
×
232
        return r.get() == rend;
×
233
      });
234

235

236
  if(it == m_renderers.end())
×
237
  {
238
    // No render list yet for this output -- either it has never been built, or
239
    // a previous attempt failed. Build it now instead of leaving the output
240
    // permanently without a renderer, but only against a usable swapchain:
241
    // setRenderSize() reaches here with no such guarantee.
242
    if(output.canRender())
×
243
      createOutputRenderList(output);
×
244
    return;
×
245
  }
246

247
  {
248
    std::shared_ptr<RenderList>& renderer = *it;
×
249
    if(renderer.get() == output.renderer())
×
250
    {
251
      auto state = output.renderState();
×
252
      if(!state)
×
253
        return;
×
254

255
      auto old_renderer = renderer;
×
256
      old_renderer->release();
×
257
      old_renderer.reset();
×
258

259
      auto new_renderer = createRenderList(&output, state);
×
260

261
      renderer = new_renderer;
×
262

263
      if(!renderer)
×
264
      {
265
        output.setRenderer({});
×
266
        it = m_renderers.erase(it);
×
267
      }
×
268
    }
×
269
    else
270
    {
271
      qDebug("???");
×
272
    }
273
  }
274
}
×
275

276
void Graph::initializeOutput(OutputNode* output, GraphicsApi graphicsApi)
×
277
{
278
  output->updateGraphicsAPI(graphicsApi);
×
279
  // Only when there is no output yet: createOutput() replaces ScreenNode's
280
  // Window outright, so calling it on a live output that merely is not ready
281
  // to render would leak its QRhi and orphan an embedded window from its
282
  // container. An output that exists but cannot render yet waits for the
283
  // onResize below instead.
284
  if(!output->renderState())
×
285
  {
286
    auto onReady = [this, output] {
×
287
      if(output->canRender())
×
288
        createOutputRenderList(*output);
×
289
    };
×
290

291
    auto onResize = [this, output] {
×
292
      // FIXME optimize if size did not change?
293
      recreateOutputRenderList(*output);
×
294
    };
×
295

296
    // TODO only works for one output !!
297
    output->createOutput({.graphicsApi = graphicsApi, .onReady = onReady, .onResize = onResize});
×
298
  }
×
299
  else if(output->canRender())
×
300
  {
301
    createOutputRenderList(*output);
×
302
    // output->window->state.hasSwapChain = true;
303
  }
×
304
}
×
305

306
void Graph::relinkGraph()
×
307
{
308
  for(auto r_it = m_renderers.begin(); r_it != m_renderers.end();)
×
309
  {
310
    auto& r = **r_it;
×
311
    for(auto& node : m_nodes)
×
312
      node->addedToGraph = false;
×
313

314
    assert(!r.nodes.empty());
×
315

316
    auto out = r.nodes.back();
×
317
    r.nodes.clear();
×
318
    r.nodes.push_back(out);
×
319

320
    r.clearRenderers();
×
321

322
    auto& model_nodes = r.nodes;
×
323
    {
324
      // In which order do we want to render stuff
325
      graphwalk(model_nodes);
×
326

327
      if(model_nodes.size() > 1)
×
328
      {
329
        bool invalid_renderlist = false;
×
330
        for(auto node : model_nodes)
×
331
        {
332
          score::gfx::NodeRenderer* rn{};
×
333
          auto it = node->renderedNodes.find(&r);
×
334
          if(it == node->renderedNodes.end())
×
335
          {
336
            if((rn = node->createRenderer(r)))
×
337
            {
338
              rn->nodeId = node->nodeId;
×
339
              node->renderedNodes.emplace(&r, rn);
×
340

341
              node->renderedNodesChanged();
×
342
              //rn->init(r);
343
            }
×
344
            else
345
            {
346
              invalid_renderlist = true;
×
347
              break;
×
348
            }
349
          }
×
350
          else
351
          {
352
            rn = it->second;
×
353
            SCORE_ASSERT(rn);
×
354
            rn->release(r);
×
355
            //rn->init(r);
356
          }
357
          SCORE_ASSERT(rn);
×
358
          r.renderers.push_back(rn);
×
359
        }
360

361
        // If a node couldn't be recreated, we skip the whole thing
362
        if(invalid_renderlist)
×
363
        {
364
          r.output.setRenderer({});
×
365
          r_it = m_renderers.erase(r_it);
×
366
          break;
×
367
        }
368

369
        //         for(auto node : r.renderers)
370
        //         {
371
        //           node->init(r);
372
        //         }
373
      }
×
374
      else if(model_nodes.size() == 1)
×
375
      {
376
        SCORE_ASSERT(
×
377
            model_nodes[0]->renderedNodes.find(&r)
378
            != model_nodes[0]->renderedNodes.end());
379
        auto rn = model_nodes[0]->renderedNodes.find(&r)->second;
×
380
        SCORE_ASSERT(rn);
×
381
        rn->release(r);
×
382
      }
×
383
    }
384
    r.output.onRendererChange();
×
385

386
    ++r_it;
×
387
  }
388

389
  if(m_outputs.size() > m_renderers.size())
×
390
  {
391
    // Try to recreate missing ones
392
    for(auto& output : m_outputs)
×
393
    {
394
      if(!output->renderer() && output->canRender())
×
395
      {
396
        createOutputRenderList(*output);
×
397
      }
×
398
    }
399
  }
×
400
}
×
401

402
bool Graph::canDoVSync() const noexcept
×
403
{
404
  return m_outputs.size() == 1
×
405
         && m_outputs[0]->configuration().supportsVSync;
×
406
}
407

408
static bool createNodeRenderer(score::gfx::Node& node, RenderList& r)
×
409
{
410
  // Register the node with the renderer
411
  if(auto rn = node.createRenderer(r))
×
412
  {
413
    rn->nodeId = node.nodeId;
×
414
    r.renderers.push_back(rn);
×
415

416
    // Register the rendered nodes with their parents
417
    SCORE_ASSERT(node.renderedNodes.find(&r) == node.renderedNodes.end());
×
418
    node.renderedNodes.emplace(&r, rn);
×
419
    node.renderedNodesChanged();
×
420
    return true;
×
421
  }
422

423
  return false;
×
424
}
×
425

426
std::shared_ptr<RenderList>
427
Graph::createRenderList(OutputNode* output, std::shared_ptr<RenderState> state)
×
428
{
429
  auto ptr = std::make_shared<RenderList>(*output, state);
×
430
  state->renderer = ptr;
×
431
  output->setRenderer(ptr);
×
432
  for(auto& node : m_nodes)
×
433
    node->addedToGraph = false;
×
434
#if 0
435
  for(auto& model : m_nodes)
436
    qDebug() << "Model: " << typeid(*model).name();
437
  for(auto node : m_nodes)
438
  {
439
    qDebug() << node->nodeId << typeid(*node).name();
440
    for(auto inlet : node->input)
441
    {
442
      qDebug() << "Inlet: " << magic_enum::enum_name(inlet->type) << inlet->edges.size();
443
      for(auto edge : inlet->edges) {
444
        qDebug() << edge->source->node << " => "<< edge->sink->node;
445
      }
446
    }
447
    for(auto outlet : node->output)
448
    {
449
      qDebug() << "Outlet: " << magic_enum::enum_name(outlet->type) << outlet->edges.size();
450
      for(auto edge : outlet->edges) {
451
        qDebug() << edge->source->node << " => "<< edge->sink->node;
452
      }
453
    }
454
  }
455
  for(auto edge  : m_edges)
456
  {
457
    qDebug() << "Edge:" << edge->source->node << " => "<< edge->sink->node;
458
  }
459
#endif
460

461
  RenderList& r = *ptr;
×
462
  auto& model_nodes = r.nodes;
×
463

464
  {
465
    model_nodes.push_back(output);
×
466

467
    // In which order do we want to render stuff
468
    graphwalk(model_nodes);
×
469

470
    // Now we have the nodes in the order in which they are going to
471
    // be init'd (e.g. output node first to create the render targets)
472
    // We create renderers for each of them
473
    for(auto node : model_nodes)
×
474
    {
475
      if(!createNodeRenderer(*node, r))
×
476
      {
477
        output->setRenderer(nullptr);
×
478
        return {};
×
479
      }
480
    }
481
  }
482

483
  output->onRendererChange();
×
484
  {
485
    r.init();
×
486

487
    if(model_nodes.size() > 1)
×
488
    {
489
      // Create all input render targets centrally before any node init().
490
      // This ensures RTs are available regardless of init order
491
      // (matches what maybeRebuild does).
492
      r.createAllInputRenderTargets();
×
493

494
      auto batch = r.initialBatch();
×
495
      for(auto node : r.renderers)
×
496
        node->init(r, *batch);
×
497
    }
×
498
  }
499

500
  return ptr;
×
501
}
×
502

503
Graph::Graph() { }
20✔
504

505
Graph::~Graph()
20✔
506
{
507
  for(auto& renderer : m_renderers)
20✔
508
  {
509
    renderer->release();
×
510
  }
511

512
  for(auto out : m_outputs)
20✔
513
  {
514
    out->destroyOutput();
×
515
  }
516

517
  clearEdges();
20✔
518
}
20✔
519

520
void Graph::addNode(Node* n)
×
521
{
522
  m_nodes.push_back(n);
×
523
}
×
524

525
void Graph::removeNode(Node* n)
×
526
{
527
  ossia::remove_erase(m_nodes, n);
×
528
}
×
529

530
void Graph::clearEdges()
20✔
531
{
532
  for(auto edge : m_edges)
20✔
533
  {
534
    delete edge;
×
535
  }
536
  m_edges.clear();
20✔
537
}
20✔
538

539
void Graph::addEdge(Port* source, Port* sink, Process::CableType t)
×
540
{
541
  auto it = ossia::find_if(
×
542
      m_edges, [=](Edge* e) { return e->source == source && e->sink == sink; });
×
543

544
  if(it == m_edges.end())
×
545
  {
546
    m_edges.push_back(new Edge{source, sink, t});
×
547
  }
×
548
  else
549
  {
550
    (*it)->type = t;
×
551
#if defined(SCORE_DEBUG)
552
    qDebug() << "Tried to add edge between " << source << sink << "\n   ==> "
×
553
             << typeid(*source->node).name() << typeid(*sink->node).name();
×
554
#endif
555
  }
556
}
×
557

558
void Graph::removeEdge(Port* source, Port* sink)
×
559
{
560
  auto it = ossia::find_if(
×
561
      m_edges, [=](Edge* e) { return e->source == source && e->sink == sink; });
×
562
  if(it != m_edges.end())
×
563
  {
564
    delete *it;
×
565
    m_edges.erase(it);
×
566
  }
×
567
}
×
568

569
void Graph::addAndLinkEdge(Port* source, Port* sink, Process::CableType t)
×
570
{
571
  addEdge(source, sink, t);
×
572

573
  auto output = dynamic_cast<OutputNode*>(sink->node);
×
574
  SCORE_ASSERT(output);
×
575

576
  recreateOutputRenderList(*output);
×
577
}
×
578

579
void Graph::unlinkAndRemoveEdge(Port* source, Port* sink)
×
580
{
581
  removeEdge(source, sink);
×
582
  auto output = dynamic_cast<OutputNode*>(sink->node);
×
583
  SCORE_ASSERT(output);
×
584

585
  recreateOutputRenderList(*output);
×
586
}
×
587

588
void Graph::destroyOutputRenderList(score::gfx::OutputNode& output)
×
589
{
590
  auto it = ossia::find_if(
×
591
      m_renderers, [rend = output.renderer()](const std::shared_ptr<RenderList>& r) {
×
592
        return r.get() == rend;
×
593
      });
594

595
  if(it != m_renderers.end())
×
596
  {
597
    std::shared_ptr<RenderList>& renderer = *it;
×
598
    if(renderer.get() == output.renderer())
×
599
    {
600
      renderer->release();
×
601
      renderer.reset();
×
602

603
      output.setRenderer({});
×
604
      it = m_renderers.erase(it);
×
605
    }
×
606
    else
607
    {
608
      qDebug("???");
×
609
    }
610
  }
×
611

612
  output.destroyOutput();
×
613
  ossia::remove_erase(m_outputs, &output);
×
614
}
×
615

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