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

ossia / score / 35952114601

24 Sep 2026 03:35AM UTC coverage: 41.187% (-0.3%) from 41.488%
35952114601

push

github

jcelerier
gfx: keep the driver's defects in one place, per render list

An empty render pass below the sample count of an earlier rasterising one
in the same command buffer loses the device on NVIDIA + Vulkan. Qt Quick's
layers have their own sample count and stay empty until their scene has
content, so a Quick node downstream of a shader node records exactly that
pair. Give each render list a Quirks it can ask, and split the command
buffer before handing it to Quick. SCORE_GFX_NO_QUIRKS=1 turns the
work-arounds off.

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

9 of 14 new or added lines in 3 files covered. (64.29%)

1113 existing lines in 9 files now uncovered.

122373 of 297112 relevant lines covered (41.19%)

113120.13 hits per line

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

71.57
/src/plugins/score-plugin-js/JS/JSProcessModel.cpp
1
// This is an open source non-commercial project. Dear PVS-Studio, please check
2
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3

4
#include "JSProcessModel.hpp"
5

6
#include <State/Expression.hpp>
7

8
#include <Process/Dataflow/Port.hpp>
9
#include <Process/ExternalFiles.hpp>
10
#include <Process/PresetHelpers.hpp>
11

12
#include <JS/ApplicationPlugin.hpp>
13
#include <JS/Commands/EditScript.hpp>
14
#include <JS/Executor/ExecutionHelpers.hpp>
15
#include <JS/JSProcessMetadata.hpp>
16
#include <JS/Qml/QmlObjects.hpp>
17
#include <Library/LibrarySettings.hpp>
18

19
#include <score/application/GUIApplicationContext.hpp>
20
#include <score/command/Dispatchers/MultiOngoingCommandDispatcher.hpp>
21
#include <score/command/Dispatchers/SingleOngoingCommandDispatcher.hpp>
22
#include <score/document/DocumentInterface.hpp>
23
#include <score/model/Identifier.hpp>
24
#include <score/serialization/JSONVisitor.hpp>
25
#include <score/serialization/VisitorCommon.hpp>
26
#include <score/tools/DeleteAll.hpp>
27
#include <score/tools/File.hpp>
28

29
#include <optional>
30

31
#include <core/document/Document.hpp>
32

33
#include <QDebug>
34
#include <QDir>
35
#include <QFileInfo>
36
#include <QFileSystemWatcher>
37
#include <QGuiApplication>
38
#include <QPointer>
39
#include <QQmlComponent>
40
#include <QQuickItem>
41
#include <QQuickWindow>
42
#include <QStandardPaths>
43

44
#include <wobjectimpl.h>
45

46
#include <vector>
47
W_OBJECT_IMPL(JS::ProcessModel)
5,238✔
48
namespace JS
49
{
50
static constexpr const char* default_js_program =
51
    R"_(import Score
52
import QtQuick
53
// This is a minimal example script that showcases the available API.
54
// View the complete documentation at
55
// https://ossia.io/score-docs/processes/javascript.html
56
Script {
57
  ValueInlet { id: in1; objectName: "Value In" }
58
  ValueOutlet { id: out1; objectName: "Value Out" }
59
  FloatSlider { id: sl; min: 10; max: 100; objectName: "Control" }
60

61
  // Called on every tick
62
  tick: function(token, state)
63
  {
64
    if (typeof in1.value !== 'undefined')
65
    {
66
      console.log(in1.value);
67
      out1.value = in1.value * mx + sl.value * my;
68
    }
69
  }
70
})_";
71
static constexpr const char* default_js_ui =
72
    R"_()_";
73

74
ProcessModel::ProcessModel(
65✔
75
    const TimeVal& duration, const QString& data, const Id<Process::ProcessModel>& id,
76
    QObject* parent)
77
    : Process::ProcessModel{
65✔
78
        duration, id, Metadata<ObjectKey_k, ProcessModel>::get(), parent}
65✔
79
{
65✔
80
  if(data.isEmpty())
65✔
81
  {
82
    (void)setProgram({default_js_program, default_js_ui});
46✔
83
  }
46✔
84
  else
85
  {
86
    if(!data.endsWith(".qml")) {
19✔
87
      (void)setProgram({data, {}});
1✔
88
    }
1✔
89
    else {
90
      m_root = data;
18✔
91
      (void)setProgram(readProgramFromFile(data));
18✔
92
    }
93
  }
94

95
  metadata().setInstanceName(*this);
65✔
UNCOV
96
}
×
97

98
Process::ProcessFlags ProcessModel::flags() const noexcept
1,609✔
99
{
100
  auto flags = Metadata<Process::ProcessFlags_k, JS::ProcessModel>::get();
1,609✔
101
  if(m_ui_component)
1,609✔
102
    // The UI is a Qt Quick scene in a window container: it can be docked.
103
    flags |= Process::ExternalUIAvailable | Process::ExternalUIEmbeddable;
200✔
104
  return flags;
1,609✔
105
}
106

107
ProcessModel::~ProcessModel()
150✔
108
{
75✔
109
  if(this->externalUI)
75✔
110
  {
111
    this->externalUI->close();
1✔
112
    this->externalUI = nullptr;
1✔
113
  }
1✔
114
}
150✔
115

116
void ProcessModel::mapExternalFiles(Process::ExternalFileMap& map)
8✔
117
{
118
  Process::ProcessModel::mapExternalFiles(map);
8✔
119

120
  auto& ctx = score::IDocument::documentContext(*this);
8✔
121

122
  // The QML import root. Relocating it means collecting a whole include tree,
123
  // which score does not attempt: report it so the user knows the other
124
  // machine needs it.
125
  if(!m_root.isEmpty())
8✔
126
    map.readOnly(m_root, score::FileKind::Script);
5✔
127

128
  // A script is stored either inline or as a path to a .qml file; only the
129
  // latter is an external dependency.
130
  QmlSource next = m_program;
8✔
131
  bool changed = false;
8✔
132
  const auto relocate = [&](QString& script) {
24✔
133
    if(!Process::looksLikeExistingFile(script, ctx))
16✔
134
      return;
16✔
135

UNCOV
136
    const QString relocated = map.map(
×
UNCOV
137
        {.path = script,
×
138
         .kind = score::FileKind::Script,
139
         .usage = Process::FileUsage::Input,
140
         .directory = false,
141
         .rewritable = true,
UNCOV
142
         .owner = map.owner});
×
143
    if(relocated.isEmpty())
×
144
      return;
×
145

UNCOV
146
    script = relocated;
×
UNCOV
147
    changed = true;
×
148
  };
16✔
149
  relocate(next.execution);
8✔
150
  relocate(next.ui);
8✔
151

152
  if(changed)
8✔
UNCOV
153
    map.addCommand(new JS::EditScript{*this, next, ctx});
×
154
}
8✔
155

156
QString ProcessModel::uiPathFor(const QString& qmlPath) noexcept
58✔
157
{
158
  if(!qmlPath.endsWith(".qml", Qt::CaseInsensitive))
58✔
UNCOV
159
    return {};
×
160
  return qmlPath.chopped(3) + QStringLiteral("ui.qml");
58✔
161
}
58✔
162

163
QmlSource ProcessModel::readProgramFromFile(const QString& qmlPath) noexcept
58✔
164
{
165
  const auto read = [](const QString& path) -> QString {
174✔
166
    if(path.isEmpty())
116✔
UNCOV
167
      return {};
×
168
    QFile f{path};
116✔
169
    if(!f.open(QIODevice::ReadOnly))
116✔
170
      return {};
28✔
171
    return QString::fromUtf8(f.readAll());
88✔
172
  };
116✔
173
  return {read(qmlPath), read(uiPathFor(qmlPath))};
58✔
174
}
175

176
void ProcessModel::updateFileLink() noexcept
130✔
177
{
178
  const auto file = m_root.isEmpty() ? QmlSource{} : readProgramFromFile(m_root);
130✔
179
  m_modified = m_root.isEmpty() || m_program != file;
130✔
180

181
  // An unreadable file has nothing to offer: leave the copy the document holds
182
  // alone rather than proposing to replace it with nothing.
183
  const bool outOfDate = m_modified && !file.execution.isEmpty();
130✔
184
  if(outOfDate != m_outOfDate)
130✔
185
  {
186
    m_outOfDate = outOfDate;
13✔
187
    externalSourceOutOfDateChanged(outOfDate);
13✔
188
  }
13✔
189
}
130✔
190

191
score::Command*
192
ProcessModel::refreshFromExternalSource(const score::DocumentContext& ctx) const
4✔
193
{
194
  if(!m_outOfDate)
4✔
195
    return nullptr;
3✔
196

197
  auto next = readProgramFromFile(m_root);
1✔
198
  if(next.execution.isEmpty())
1✔
UNCOV
199
    return nullptr;
×
200

201
  return new JS::EditScript{*this, std::move(next), ctx};
1✔
202
}
4✔
203

204
QString ProcessModel::rootPath() const noexcept
144✔
205
{
206
  if(!m_root.isEmpty())
144✔
207
  {
208
    return m_root;
54✔
209
  }
210
  else
211
  {
212
    // Not cached: tests run several applications in one process
213
    const auto& lib = score::AppContext().settings<Library::Settings::Model>();
90✔
214

215
    return lib.getDefaultLibraryPath() + QDir::separator() + "Scripts"
180✔
216
           + QDir::separator() + "include" + QDir::separator() + "Script/Script.qml";
90✔
217
  }
218
}
144✔
219

220
bool ProcessModel::validate(const std::vector<QString>& script) const noexcept
6✔
221
{
222
  if(script.empty())
6✔
UNCOV
223
    return false;
×
224
  if(script[0].isEmpty())
6✔
UNCOV
225
    return false;
×
226

227
  const auto trimmed = script[0].trimmed();
6✔
228
  const QByteArray data = trimmed.toUtf8();
6✔
229

230
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
6✔
231

232
  if(QFileInfo::exists(path))
6✔
233
  {
UNCOV
234
    return (bool)m_cache.getExecution(*this, path.toUtf8(), true);
×
235
  }
236
  else
237
  {
238
    if(!data.startsWith("import"))
6✔
UNCOV
239
      return false;
×
240
    return (bool)m_cache.getExecution(*this, data, false);
6✔
241
  }
242
}
6✔
243

244

245
// A script loaded from a .qml file is identified by that file path;
246
// otherwise by its text
247
QString ProcessModel::effect() const noexcept
304✔
248
{
249
  if(!m_root.isEmpty())
304✔
250
    return m_root;
99✔
251
  return m_qmlData;
205✔
252
}
304✔
253

254
QQuickItem* ProcessModel::createItemForUI(const score::DocumentContext& ctx) const noexcept
32✔
255
{
256
  if(!m_ui_component)
32✔
257
    return nullptr;
3✔
258
  auto& dummyEngine =  ctx.app
29✔
259
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
29✔
260
                          .m_scriptProcessUIEngine;
29✔
261

262
  auto obj = m_ui_component->beginCreate(dummyEngine.rootContext());
29✔
263

264
  if(!obj)
29✔
UNCOV
265
    return nullptr;
×
266
  auto script = qobject_cast<ScriptUI*>(obj);
29✔
267
  auto self = const_cast<JS::ProcessModel*>(this);
29✔
268
  if(script) {
29✔
269
    script->setProcess(self);
29✔
270
  }
29✔
271
  m_ui_component->completeCreate();
29✔
272

273
  if(!script) {
29✔
274
    delete obj;
×
275
    return nullptr;
×
276
  }
277

278
  if(const auto& on_exec = script->executionEvent(); on_exec.isCallable())
29✔
279
  {
280
    connect(this, &JS::ProcessModel::executionToUi,
×
281
            script, [&dummyEngine, on_exec] (const QVariant& v) {
×
UNCOV
282
      on_exec.call({dummyEngine.toScriptValue(v)});
×
UNCOV
283
    });
×
UNCOV
284
  }
×
285

286
  connect(script, &ScriptUI::executionSend, this, [self](const QJSValue& v) {
29✔
UNCOV
287
    self->uiToExecution(v.toVariant());
×
UNCOV
288
  });
×
289

290
  struct StateUpdater
291
  {
292
    const score::DocumentContext& ctx;
293
    JS::ProcessModel& self;
294
    std::unique_ptr<MultiOngoingCommandDispatcher> disp;
295
    int count = 0;
296

297
    void beginUpdateState(const QString& name)
×
298
    {
299
      if(count > 0) {
×
UNCOV
300
        count++;
×
301
      }
×
302
      else {
303
        disp = std::make_unique<MultiOngoingCommandDispatcher>(ctx.commandStack);
×
304
        count = 1;
×
305
      }
306
    }
×
307

UNCOV
308
    void endUpdateState()
×
309
    {
310
      if(!disp)
×
311
        return;
×
312
      count--;
×
UNCOV
313
      if(count > 0)
×
314
        return;
×
315

316
      disp->commit<JS::UpdateStateMacro>();
×
UNCOV
317
      disp.reset();
×
318
      count = 0;
×
UNCOV
319
    }
×
320

321
    void updateState(const QString& k, const QJSValue& v)
×
322
    {
323
      beginUpdateState("Update");
×
324

325
      disp->submit<JS::UpdateStateElement>(self, k, ossia::qt::value_from_js(v));
×
326

UNCOV
327
      endUpdateState();
×
328
    }
×
329

330
    void cancelUpdateState()
×
331
    {
UNCOV
332
      if(!disp)
×
333
        return;
×
334

335
      disp->rollback();
×
UNCOV
336
      disp.reset();
×
337
      count = 0;
×
UNCOV
338
    }
×
339

340
    void clearState()
×
341
    {
342
      beginUpdateState("Clear");
×
343

344
      disp->submit<JS::ReplaceState>(self, JS::JSState{});
×
345

346
      endUpdateState();
×
347
    }
×
348

349
    void replaceState(const QJSValue& v)
×
350
    {
351
      beginUpdateState("Replace");
×
352

353
      JS::JSState cur;
×
UNCOV
354
      auto var = v.toVariant().toMap();
×
355
      for(auto it = var.constBegin(); it != var.constEnd(); ++it) {
×
356
        if(it.value().isValid()) {
×
UNCOV
357
          cur.insert_or_assign(it.key(), ossia::qt::qt_to_ossia{}(it.value()));
×
UNCOV
358
        }
×
UNCOV
359
      }
×
UNCOV
360
      disp->submit<JS::ReplaceState>(self, std::move(cur));
×
361

362
      endUpdateState();
×
363
    }
×
364
  };
365
  auto updater = std::make_shared<StateUpdater>(StateUpdater{ctx, *self});
29✔
366

367
  connect(script, &ScriptUI::beginUpdateState,
58✔
368
          this, [updater] (const QString& name) {
29✔
UNCOV
369
    updater->beginUpdateState(name);
×
370
  });
×
371
  connect(script, &ScriptUI::updateState,
58✔
372
          this, [updater] (const QString& name, const QJSValue& v) {
29✔
UNCOV
373
    updater->updateState(name, v);
×
374
  });
×
375
  connect(script, &ScriptUI::endUpdateState,
58✔
376
          this, [updater] () {
29✔
UNCOV
377
    updater->endUpdateState();
×
378
  });
×
379
  connect(script, &ScriptUI::cancelUpdateState,
58✔
380
          this, [updater] () {
29✔
UNCOV
381
    updater->cancelUpdateState();
×
382
  });
×
383
  connect(script, &ScriptUI::clearState,
58✔
384
          this, [updater] () {
29✔
UNCOV
385
    updater->clearState();
×
UNCOV
386
  });
×
387
  connect(script, &ScriptUI::replaceState,
58✔
388
          this, [updater] (const QJSValue& v) {
29✔
389
    updater->replaceState(v);
×
390
  });
×
391

392
  if(const auto& on_stateUpdated = script->stateUpdated(); on_stateUpdated.isCallable())
29✔
393
  {
UNCOV
394
    connect(
×
395
        this, &JS::ProcessModel::stateElementChanged, script,
×
396
        [on_stateUpdated, &dummyEngine](const QString& k, const ossia::value& v) {
×
UNCOV
397
      if(v.valid())
×
398
      {
399
        if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
400
          on_stateUpdated.call({k, dummyEngine.toScriptValue(res)});
×
401
        else
UNCOV
402
          on_stateUpdated.call({k, QJSValue{}});
×
UNCOV
403
      }
×
404
      else
405
        on_stateUpdated.call({k, QJSValue{}});
×
406
    }, Qt::QueuedConnection);
×
407
  }
×
408

409
  if(const auto& on_load = script->loadState(); on_load.isCallable())
29✔
410
  {
UNCOV
411
    QVariantMap vm;
×
UNCOV
412
    for(auto& [k, v]: this->m_state) {
×
UNCOV
413
      if(auto res = v.apply(ossia::qt::ossia_to_qvariant{}); res.isValid())
×
UNCOV
414
        vm[k] = std::move(res);
×
415
    }
UNCOV
416
    on_load.call({dummyEngine.toScriptValue(vm)});
×
UNCOV
417
  }
×
418

419
  return script;
29✔
420
}
32✔
421

422
QWidget* ProcessModel::createWindowForUI(const score::DocumentContext& ctx,
23✔
423
                                         QWidget* parent) const noexcept
424
{
425
  m_ui_object = createItemForUI(ctx);
23✔
426
  if(!m_ui_object)
23✔
UNCOV
427
    return nullptr;
×
428

429
  // A close is deferred: by the time this runs the process may show a new
430
  // UI, which must be left alone. The container pointer is only compared,
431
  // the widget may be half-destroyed.
432
  const auto cleanup_ui = [this](QQuickWindow* win, QWidget* container) {
45✔
433
    if(m_ui_object && m_ui_object->window() == win)
22✔
434
    {
435
      delete m_ui_object;
×
436
      m_ui_object = nullptr;
×
UNCOV
437
    }
×
438

439
    if(externalUI == container)
22✔
440
    {
UNCOV
441
      const_cast<QWidget*&>(externalUI) = nullptr;
×
UNCOV
442
      externalUIVisible(false);
×
UNCOV
443
    }
×
444
  };
22✔
445

446
  // Let the UI take the whole window: needed when the window is docked in
447
  // the main window and follows the size of its pane.
448
  const auto fitToWindow = [](QQuickWindow* win, QQuickItem* item) {
50✔
449
    if(win && item)
27✔
450
      item->setSize(QSizeF(win->width(), win->height()));
27✔
451
  };
27✔
452

453
  // On a new compile the UI object is recreated; put it back in the window
454
  const auto reloadUI = [this, &ctx, fitToWindow](QQuickWindow* win) -> bool {
26✔
455
    // A window closed and waiting for its deletion while a new one holds
456
    // the UI: nothing to do in it
457
    if(m_ui_object && m_ui_object->window() != win)
3✔
UNCOV
458
      return true;
×
459

460
    delete m_ui_object;
3✔
461
    m_ui_object = nullptr;
3✔
462
    if(!m_ui_component)
3✔
UNCOV
463
      return false;
×
464

465
    m_ui_object = createItemForUI(ctx);
3✔
466
    if(!m_ui_object)
3✔
UNCOV
467
      return false;
×
468
    m_ui_object->setParentItem(win->contentItem());
3✔
469
    m_ui_object->setParent(win->contentItem());
3✔
470
    fitToWindow(win, m_ui_object);
3✔
471
    return true;
3✔
472
  };
3✔
473

474
  // The requested size: the ScriptUI root asks for one through its implicit
475
  // width / height (e.g. `implicitWidth: 1280`), defaulting to 640x640.
476
  const QSize requested = [this] {
46✔
477
    int w = 640, h = 640;
23✔
478
    if(m_ui_object->implicitWidth() >= 100.)
23✔
479
      w = static_cast<int>(m_ui_object->implicitWidth());
20✔
480
    if(m_ui_object->implicitHeight() >= 100.)
23✔
481
      h = static_cast<int>(m_ui_object->implicitHeight());
20✔
482
    return QSize{w, h};
23✔
483
  }();
484

485
  auto win = new QQuickWindow{};
23✔
486
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
487
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
488
  // leaves the window with no title bar, close or minimise button.
489
  win->setFlags(
23✔
490
      win->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
23✔
491
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
23✔
492
      | Qt::WindowMaximizeButtonHint);
23✔
493
#if defined(__EMSCRIPTEN__)
494
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
495
  // turns into showFullScreen() for every top level: the requested size is
496
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
497
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
498
  win->setFlags(win->flags() | Qt::Dialog);
499
#endif
500
  win->setWidth(640);
23✔
501
  win->setHeight(640);
23✔
502
  win->setColor(qApp->palette().color(QPalette::Window));
23✔
503

504
  m_ui_object->setParentItem(win->contentItem());
23✔
505
  connect(win, &QQuickWindow::widthChanged, this, [this, win, fitToWindow] {
35✔
506
    fitToWindow(win, m_ui_object);
12✔
507
  });
12✔
508
  connect(win, &QQuickWindow::heightChanged, this, [this, win, fitToWindow] {
35✔
509
    fitToWindow(win, m_ui_object);
12✔
510
  });
12✔
511

512
  auto widg = QWidget::createWindowContainer(win, parent);
23✔
513
  if(!widg) {
23✔
UNCOV
514
    delete m_ui_object;
×
UNCOV
515
    m_ui_object = nullptr;
×
UNCOV
516
    delete win;
×
UNCOV
517
    return nullptr;
×
518
  }
519
  widg->setAttribute(Qt::WA_DeleteOnClose);
23✔
520
  // The container widget does not follow the QQuickWindow's size
521
  widg->resize(requested);
23✔
522

523
#if QT_VERSION >= QT_VERSION_CHECK(6,8,2)
524
  // Bug in older Qt 6 versions:
525
  // QtCore/qmetatype.h:842:23: error: invalid application of 'sizeof' to an incomplete type 'QQuickCloseEvent'
526
  // static_assert(sizeof(T), "Type argument of Q_PROPERTY or Q_DECLARE_METATYPE(T*) must be fully defined");
527
  connect(win, &QQuickWindow::closing, this, [cleanup_ui, win, widg] {
528
    cleanup_ui(win, widg);
529
  });
530
#endif
531
  connect(win, &QQuickWindow::destroyed, this, [cleanup_ui, win, widg] {
45✔
532
    cleanup_ui(win, widg);
22✔
533
  });
22✔
534
  connect(
23✔
535
      this, &JS::ProcessModel::uiScriptOk, win,
23✔
536
      [reloadUI, win, container = QPointer<QWidget>{widg}] {
26✔
537
    if(!reloadUI(win))
3✔
538
    {
539
      // The container is what the main window holds: closing it takes the
540
      // window with it and runs the cleanup from destroyed()
UNCOV
541
      if(container)
×
UNCOV
542
        container->close();
×
UNCOV
543
    }
×
544
  });
3✔
545

546
  // No externalUIVisible(true) here: setupExternalUI emits it for every
547
  // process type once the UI is placed, and only when one is actually created
548
  // -- asking again for an open UI raises it instead. Emitting a second time
549
  // from this path would make a re-request report {true, true, false}.
550
  return widg;
23✔
551
}
23✔
552

553
void ProcessModel::setExecutionScript(const QString& f)
123✔
554
{
555
  if(f == m_program.execution)
123✔
556
    return;
13✔
557
  m_program.execution = std::move(f);
110✔
558

559
  executionScriptChanged(m_program.execution);
110✔
560
}
123✔
561

562
void ProcessModel::setUiScript(const QString& f)
123✔
563
{
564
  if(f == m_program.ui)
123✔
565
    return;
90✔
566
  m_program.ui = std::move(f);
33✔
567

568
  uiScriptChanged(m_program.ui);
33✔
569
}
123✔
570

571
void ProcessModel::setState(const JSState &s)
31✔
572
{
573
  if(s == m_state)
31✔
574
    return;
13✔
575

576
  {
577
    const auto prev = std::move(m_state);
18✔
578
    for(auto& [prev_k, prev_v] : prev) {
35✔
579
      stateElementChanged(prev_k, prev_v);
17✔
580
    }
581
  }
18✔
582

583
  m_state = std::move(s);
18✔
584
  for(auto& [k, v] : m_state) {
46✔
585
    stateElementChanged(k, v);
28✔
586
  }
587

588
  stateChanged();
18✔
589
}
31✔
590

UNCOV
591
void ProcessModel::updateState(const QString &k, const ossia::value& res)
×
592
{
593
  if(auto it = m_state.find(k); it != m_state.end())
×
594
  {
595
    if(res.valid())
×
596
    {
597
      if(res != it->second)
×
598
      {
599
        // Updating a new element
UNCOV
600
        m_state[k] = res;
×
601
        stateElementChanged(k, res);
×
602
        stateChanged();
×
603
      }
×
UNCOV
604
    }
×
605
    else
606
    {
607
      // Removing an element
608
      m_state.erase(k);
×
UNCOV
609
      stateElementChanged(k, res);
×
UNCOV
610
      stateChanged();
×
611
    }
612
  }
×
613
  else
614
  {
UNCOV
615
    if(res.valid())
×
616
    {
617
      // Adding a new element
UNCOV
618
      m_state[k] = res;
×
UNCOV
619
      stateElementChanged(k, res);
×
620
      stateChanged();
×
UNCOV
621
    }
×
622
    else
623
    {
624
      // Already not there, nothing to do
625
    }
626
  }
UNCOV
627
}
×
628

629
[[nodiscard]] Process::ScriptChangeResult ProcessModel::setProgram(const JS::QmlSource& script)
123✔
630
{
631
  setExecutionScript(script.execution);
123✔
632
  setUiScript(script.ui);
123✔
633

634
  Process::ScriptChangeResult res;
123✔
635
  const auto trimmed = script.execution.trimmed();
123✔
636
  const QByteArray data = trimmed.toUtf8();
123✔
637

638
  auto path = score::locateFilePath(trimmed, score::IDocument::documentContext(*this));
123✔
639

640
  // setQmlData builds the UI component from m_program.ui: it must be the new
641
  // one, otherwise every compile shows the UI of the previous compile.
642
  const auto previous = m_program;
123✔
643
  m_program = script;
123✔
644

645
  if(QFileInfo::exists(path))
123✔
UNCOV
646
    res = setQmlData(path.toUtf8(), true);
×
647
  else
648
    res = setQmlData(data, false);
123✔
649

650
  if(!res.valid)
123✔
651
    m_program = previous;
3✔
652
  updateFileLink();
123✔
653
  return res;
123✔
654
}
123✔
655

656
Process::ScriptChangeResult ProcessModel::setQmlData(const QByteArray& data, bool isFile)
123✔
657
{
658
  Process::ScriptChangeResult res;
123✔
659
  if(!isFile && !data.contains("import "))
123✔
UNCOV
660
    return res;
×
661

662
  auto script = m_cache.getExecution(*this, data, isFile);
123✔
663
  if(!script)
123✔
664
    return res;
3✔
665

666
  m_isFile = isFile;
120✔
667
  m_qmlData = data;
120✔
668

669
  res.inlets = score::clearAndDeleteLater(m_inlets);
120✔
670
  res.outlets = score::clearAndDeleteLater(m_outlets);
120✔
671
  const bool had_ui = m_ui_component;
120✔
672
  m_ui_component = nullptr;
120✔
673
  delete m_ui_object;
120✔
674
  m_ui_object = nullptr;
120✔
675

676
  SCORE_ASSERT(m_inlets.size() == 0);
120✔
677
  SCORE_ASSERT(m_outlets.size() == 0);
120✔
678

679
  // Check inlets / outlets
680
  {
681
    auto cld_inlet = script->findChildren<Inlet*>();
120✔
682
    int i = 0;
120✔
683
    for(auto n : cld_inlet)
296✔
684
    {
685
      auto port = n->make(Id<Process::Port>(i++), this);
176✔
686
      if(const auto& name = n->objectName(); !name.isEmpty())
315✔
687
        port->setName(name);
139✔
688
      if(auto addr = State::parseAddressAccessor(n->address()))
176✔
UNCOV
689
        port->setAddress(std::move(*addr));
×
690
      m_inlets.push_back(port);
176✔
691
    }
692
  }
120✔
693

694
  {
695
    auto cld_outlet = script->findChildren<Outlet*>();
120✔
696
    int i = 0;
120✔
697
    for(auto n : cld_outlet)
218✔
698
    {
699
      auto port = n->make(Id<Process::Port>(i++), this);
98✔
700
      if(const auto& name = n->objectName(); !name.isEmpty())
163✔
701
        port->setName(name);
65✔
702
      if(auto addr = State::parseAddressAccessor(n->address()))
98✔
UNCOV
703
        port->setAddress(std::move(*addr));
×
704
      m_outlets.push_back(port);
98✔
705
    }
706
  }
120✔
707

708
  // Create ui if any
709
  if(!this->m_program.ui.isEmpty()) {
120✔
710
    m_ui_component = m_cache.getUi(*this, this->m_program.ui.toUtf8(), isFile);
39✔
711
  }
39✔
712

713
  if(m_isFile)
120✔
714
  {
UNCOV
715
    const auto name = QFileInfo{data}.baseName();
×
UNCOV
716
    metadata().setName(name);
×
UNCOV
717
    metadata().setLabel(name);
×
UNCOV
718
  }
×
719
  else if(metadata().getName().isEmpty())
120✔
720
  {
721
    metadata().setName(QStringLiteral("Script"));
63✔
722
  }
63✔
723

724
  executionScriptOk();
120✔
725
  res.valid = true;
120✔
726

727
  if(bool(m_ui_component) != had_ui)
120✔
728
    flagsChanged();
9✔
729

730
  if(m_ui_component)
120✔
731
  {
732
    uiScriptOk();
11✔
733
  }
11✔
734
  else if(externalUI)
109✔
735
  {
UNCOV
736
    externalUI->close();
×
UNCOV
737
    externalUI->deleteLater();
×
UNCOV
738
    externalUI = nullptr;
×
UNCOV
739
    externalUIVisible(false);
×
UNCOV
740
  }
×
741

742
  // inlets / outletsChanged : in ScriptEditCommand
743
  return res;
120✔
744
}
123✔
745

746
Script* ProcessModel::currentExecutionObject() const noexcept
4✔
747
{
748
  if(auto cache = m_cache.tryGet(m_qmlData, m_isFile))
4✔
749
    return cache->object.get();
2✔
750
  return nullptr;
2✔
751
}
4✔
752

753
bool ProcessModel::isGpu() const noexcept
4✔
754
{
755
#if defined(SCORE_HAS_GPU_JS)
756
  if(auto script = currentExecutionObject())
757
  {
758
    return
759
        script->findChild<JS::TextureInlet*>() != nullptr
760
           || script->findChild<JS::TextureOutlet*>() != nullptr
761
           // || script->findChild<JS::BufferInlet*>() != nullptr
762
           // || script->findChild<JS::BufferOutlet*>() != nullptr
763
        ;
764
  }
765
#endif
766
  return false;
4✔
767
}
768

769
ComponentCache::ComponentCache() { }
75✔
770
ComponentCache::~ComponentCache() { }
75✔
771

772
QByteArray ComponentCache::key(const QByteArray& str, bool isFile) noexcept
284✔
773
{
774
  if(!isFile)
284✔
775
    return str;
284✔
776

777
  // What is cached is a compiled script, so what identifies it is the text it
778
  // was compiled from -- not the name of the file that happened to hold it,
779
  // which says nothing about whether that file still says the same thing.
UNCOV
780
  QFile f{QString::fromUtf8(str)};
×
UNCOV
781
  if(!f.open(QIODevice::ReadOnly))
×
UNCOV
782
    return {};
×
UNCOV
783
  return f.readAll();
×
784
}
284✔
785

786
const ComponentCache::Cache* ComponentCache::tryGet(const QByteArray& str, bool isFile) const noexcept
172✔
787
{
788
  const QByteArray content = key(str, isFile);
172✔
789
  if(content.isEmpty())
172✔
790
    return nullptr;
2✔
791

792
  if(auto it = ossia::find_if(m_map, [&](const auto& k) { return k.key == content; });
310✔
793
     it != m_map.end())
170✔
794
  {
795
    return &*it;
27✔
796
  }
797
  return nullptr;
143✔
798
}
172✔
799

800
Script* ComponentCache::getExecution(
129✔
801
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
802
{
803
  if(auto cache = tryGet(str, isFile))
129✔
804
    return cache->object.get();
23✔
805

806
  auto& dummyEngine = score::GUIAppContext()
212✔
807
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
106✔
808
                          .m_scriptProcessUIEngine;
106✔
809
  std::unique_ptr<QQmlComponent> comp;
106✔
810
  if(!isFile)
106✔
811
  {
812
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
106✔
813
    loadJSObjectFromString(process.rootPath(), str, *comp, false);
106✔
814
  }
106✔
815
  else
816
  {
UNCOV
817
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
×
UNCOV
818
    loadJSObjectFromFile(QString::fromUtf8(str), *comp);
×
819
  }
820

821
  const auto& errs = comp->errors();
106✔
822
  if(!errs.empty())
106✔
823
  {
824
    const auto& err = errs.first();
3✔
825
    qDebug() << err.line() << err.toString();
3✔
826
    auto str = err.toString();
3✔
827
    str.remove("<Unknown File>:");
3✔
828
    process.errorMessage(/* err.line(), */str);
3✔
829
    return nullptr;
3✔
830
  }
3✔
831

832
  auto obj = comp->create();
103✔
833
  auto script = qobject_cast<JS::Script*>(obj);
103✔
834
  if(script)
103✔
835
  {
836
    if(m_map.size() > 5)
103✔
UNCOV
837
      m_map.erase(m_map.begin());
×
838

839
    m_map.emplace_back(
206✔
840
        Cache{key(str, isFile), std::move(comp), std::unique_ptr<JS::Script>(script)});
103✔
841
    return script;
103✔
842
  }
843
  else
844
  {
UNCOV
845
    process.errorMessage(/* 0, */"The component must be of type Script");
×
UNCOV
846
    if(obj)
×
847
    {
UNCOV
848
      delete obj;
×
UNCOV
849
    }
×
UNCOV
850
    return nullptr;
×
851
  }
852
}
129✔
853

854
QQmlComponent* ComponentCache::getUi(
39✔
855
    const ProcessModel& process, const QByteArray& str, bool isFile) noexcept
856
{
857
  if(auto cache = tryGet(str, isFile))
39✔
858
    return cache->component.get();
2✔
859

860
  auto& dummyEngine = score::GUIAppContext()
74✔
861
                          .guiApplicationPlugin<JS::ApplicationPlugin>()
37✔
862
                          .m_scriptProcessUIEngine;
37✔
863

864
  std::unique_ptr<QQmlComponent> comp;
37✔
865
  if(!isFile)
37✔
866
  {
867
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
37✔
868
    loadJSObjectFromString(process.rootPath(), str, *comp, true);
37✔
869
  }
37✔
870
  else
871
  {
UNCOV
872
    comp = std::make_unique<QQmlComponent>(&dummyEngine);
×
UNCOV
873
    loadJSObjectFromFile(QString::fromUtf8(str), *comp);
×
874
  }
875

876
  const auto& errs = comp->errors();
37✔
877
  if(!errs.empty())
37✔
878
  {
879
    const auto& err = errs.first();
11✔
880
    qDebug() << err.line() << err.toString();
11✔
881
    auto str = err.toString();
11✔
882
    str.remove("<Unknown File>:");
11✔
883
    process.errorMessage(/* err.line(), */str);
11✔
884
    return nullptr;
11✔
885
  }
11✔
886

887
  auto obj = comp->beginCreate(dummyEngine.rootContext());
26✔
888
  if(!obj) {
26✔
UNCOV
889
    process.errorMessage(/* 0, */"Cannot create UI object");
×
UNCOV
890
    return nullptr;
×
891
  }
892
  auto script = qobject_cast<ScriptUI*>(obj);
26✔
893
  if(script) {
26✔
894
    script->setProcess((Process::ProcessModel*)&process);
9✔
895
  }
9✔
896
  comp->completeCreate();
26✔
897
  if(script)
26✔
898
  {
899
    if(m_map.size() > 5)
9✔
UNCOV
900
      m_map.erase(m_map.begin());
×
901

902
    m_map.emplace_back(
18✔
903
        Cache{key(str, isFile), std::move(comp), {}});
9✔
904
    delete script;
9✔
905
    return m_map.back().component.get();
9✔
906
  }
907
  else
908
  {
909
    process.errorMessage(/* 0, */"The component must be of type Script");
17✔
910
    if(obj)
17✔
911
      delete obj;
17✔
912
    return nullptr;
17✔
913
  }
914
}
39✔
915

916
// Preset data: {"Controls": [...], "State": [...]} where "State" is the
917
// script state set through Script.replaceState, plus the program: "Root"
918
// (source .qml file, also used for includes) and "Script" / "Ui" when they
919
// differ from that file. A plain array of controls is also accepted; its
920
// script is then the effect of the preset key.
921
void ProcessModel::loadPreset(const Process::Preset& preset)
12✔
922
{
923
  const rapidjson::Document doc = readJson(preset.data);
12✔
924
  const bool object = doc.IsObject();
12✔
925
  if(!object && !doc.IsArray())
12✔
UNCOV
926
    return;
×
927

928
  std::optional<QmlSource> program;
12✔
929
  if(object)
12✔
930
  {
931
    QString root;
11✔
932
    if(auto it = doc.FindMember("Root"); it != doc.MemberEnd() && it->value.IsString())
13✔
933
      root = score::locateFilePath(
2✔
934
          QString::fromUtf8(it->value.GetString(), it->value.GetStringLength()),
2✔
935
          score::IDocument::documentContext(*this));
2✔
936
    m_root = root;
11✔
937

938
    if(auto it = doc.FindMember("Script"); it != doc.MemberEnd() && it->value.IsString())
21✔
939
    {
940
      QmlSource p{
10✔
941
          QString::fromUtf8(it->value.GetString(), it->value.GetStringLength()), {}};
10✔
942
      if(auto ui = doc.FindMember("Ui"); ui != doc.MemberEnd() && ui->value.IsString())
11✔
943
        p.ui = QString::fromUtf8(ui->value.GetString(), ui->value.GetStringLength());
1✔
944
      program = std::move(p);
10✔
945
    }
10✔
946
    else if(!root.isEmpty())
1✔
947
    {
948
      program = readProgramFromFile(root);
1✔
949
    }
1✔
950
  }
11✔
951
  if(!program && !preset.key.effect.isEmpty())
12✔
952
    program = QmlSource{preset.key.effect, m_program.ui};
1✔
953

954
  if(program && *program != m_program)
12✔
955
    (void)setProgram(*program);
5✔
956
  else
957
    updateFileLink();
7✔
958

959
  if(!object)
12✔
960
  {
961
    Process::loadFixedControls(doc.GetArray(), *this);
1✔
962
    return;
1✔
963
  }
964

965
  if(auto it = doc.FindMember("Controls"); it != doc.MemberEnd() && it->value.IsArray())
22✔
966
    Process::loadFixedControls(it->value.GetArray(), *this);
11✔
967

968
  JSState st;
11✔
969
  if(auto it = doc.FindMember("State"); it != doc.MemberEnd())
11✔
970
    st <<= JsonValue{it->value};
11✔
971
  setState(st);
11✔
972
}
12✔
973

974
Process::Preset ProcessModel::savePreset() const noexcept
12✔
975
{
976
  auto p = Process::saveScriptProcessPreset(*this, effect());
12✔
977

978
  JSONReader r;
12✔
979
  r.stream.StartObject();
12✔
980
  if(!m_root.isEmpty())
12✔
981
    r.obj["Root"] = score::relativizeFilePath(m_root);
2✔
982
  if(!followsRootFile())
12✔
983
  {
984
    r.obj["Script"] = m_program.execution;
11✔
985
    if(!m_program.ui.isEmpty())
11✔
986
      r.obj["Ui"] = m_program.ui;
1✔
987
  }
11✔
988
  r.stream.Key("Controls");
12✔
989
  Process::saveFixedControls(r, *this);
12✔
990
  r.obj["State"] = m_state;
12✔
991
  r.stream.EndObject();
12✔
992
  p.data = r.toByteArray();
12✔
993
  return p;
12✔
994
}
12✔
995

996
// Also matches presets that identify a file-based script by its text
997
bool ProcessModel::presetMatches(const Process::Preset& preset) const noexcept
3✔
998
{
999
  return preset.key.key == concreteKey()
6✔
1000
         && (preset.key.effect == effect() || preset.key.effect == QString{m_qmlData});
3✔
1001
}
1002

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