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

ossia / score / 38475390298

13 Oct 2026 09:56PM UTC coverage: 48.898% (+0.8%) from 48.076%
38475390298

push

github

jcelerier
gfx: commit the initial batch before a frame can release what it names

RenderList::renderImpl carried m_initialBatch, holding every renderer's
init()-time uploads, into the frame's first pass. When an inlet spec changed
before that first frame, the surgical render-target update released and
re-initialised the renderer first, and releaseState() deletes its material
UBO outright, so the batch then named a freed QRhiBuffer. On OpenGL,
enqueueResourceUpdates copies a uniform-buffer update into the buffer's
CPU-side store: a write into freed memory, or, after destroy() cleared it,
into QByteArray's static empty storage. test_gfx_input_sampler_filter
(raw raster, OpenGL) crashed there, or read back black, depending on what
reused the allocation.

The batch is now committed to the command buffer as soon as the frame takes
it, as onEdgeRemoved() and Graph::relinkGraph() already flush it before
releasing renderer state.

The new case in test_gfx_outside_frame_release_buffer re-inits a probe
renderer on the first frame and checks, from its releaseState(), whether the
batch its upload went into still holds it: deterministic, on every backend.

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

2 of 2 new or added lines in 1 file covered. (100.0%)

6331 existing lines in 89 files now uncovered.

160459 of 328149 relevant lines covered (48.9%)

146726.48 hits per line

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

87.75
/src/plugins/score-plugin-js/JS/ApplicationPlugin.cpp
1
#include "ApplicationPlugin.hpp"
2

3
#include <JS/DocumentPlugin.hpp>
4
#include <JS/Qml/DeviceContext.hpp>
5
#include <JS/Qml/ScriptableNames.hpp>
6
#include <JS/Qml/EditContext.hpp>
7
#include <JS/Qml/Utils.hpp>
8
#include <JS/Qml/ViewContext.hpp>
9
#include <JS/ThreadLocalQmlEngine.hpp>
10
#include <Library/LibrarySettings.hpp>
11
#include <LocalTree/LocalTreeDocumentPlugin.hpp>
12

13
#include <core/application/ApplicationInterface.hpp>
14
#include <core/document/Document.hpp>
15
#include <core/presenter/DocumentManager.hpp>
16

17
#include <ossia/detail/thread.hpp>
18

19
#include <ossia-qt/invoke.hpp>
20
#include <ossia-qt/qml_protocols.hpp>
21

22
#include <QCommandLineParser>
23
#include <QFile>
24
#include <QFileInfo>
25
#include <QJsonArray>
26
#include <QJsonDocument>
27
#include <QString>
28

29
#if __has_include(<QQuickWindow>)
30
#include <QGuiApplication>
31
#include <QQuickItem>
32
#include <QQuickWindow>
33
#endif
34

35
#if SCORE_HAS_GPU_JS
36
#include <Gfx/Settings/Model.hpp>
37
#endif
38

39
#include <ossia/network/context.hpp>
40
#include <JS/Qml/VariantToJs.hpp>
41

42
namespace JS
43
{
44
// Check whether the input is a script, or a file path.
45
// An existing file always wins: a real path may legitimately contain
46
// characters (parentheses, braces, ...) that also occur in inline source,
47
// so the file-existence check must come FIRST. Only when the input is not
48
// an existing file do we fall back to the inline-source heuristic.
49
static bool stringIsScript(const QString& input)
104 ✔
50
{
51
  if(input.isEmpty())
104 ✔
UNCOV
52
    return false;
×
53

54
  if(QFileInfo fileInfo{input}; fileInfo.exists() && fileInfo.isFile())
204 ✔
55
    return false;
100 ✔
56

57
  if(input.length() > 4096)
4 ✔
UNCOV
58
    return true;
×
59

60
  for(QChar ch : input)
56 ✔
61
  {
62
    const char16_t c = ch.unicode();
55 ✔
63
    if(c == '\n' || c == '\r' || c == ';' || c == '{' || c == '}' || c == '('
55 ✔
64
       || c == ')')
55 ✔
65
      return true;
3 ✔
66
  }
67

68
  return false;
1 ✔
69
}
104 ✔
70

71
ApplicationPlugin::ApplicationPlugin(const score::GUIApplicationContext& ctx)
3,501 ✔
72
    : score::GUIApplicationPlugin{ctx}
3,501 ✔
73
{
3,501 ✔
74
#if __has_include(<QQuickWindow>) && !defined(__APPLE__)
75
  // Crisp text in every QML UI: distance-field rendering looks blurry at the small
76
  // font sizes our panels use, native glyph rendering matches the rest of score.
77
  // Not on macOS: https://qt-project.atlassian.net/browse/QTBUG-150490
78
  QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering);
3,501 ✔
79
#endif
80

81
  // For the console
82
  auto edit = new EditJsContext;
3,501 ✔
83
  m_consoleEngine.globalObject().setProperty("Score", m_consoleEngine.newQObject(edit));
3,501 ✔
84
  m_consoleEngine.globalObject().setProperty("Util", m_consoleEngine.newQObject(new JsUtils));
3,501 ✔
85
  m_consoleEngine.globalObject().setProperty(
7,002 ✔
86
      "System", m_consoleEngine.newQObject(new JsSystem));
3,501 ✔
87
  m_consoleEngine.globalObject().setProperty(
7,002 ✔
88
      "Library", m_consoleEngine.newQObject(new JsLibrary));
3,501 ✔
89
  {
90
    auto device = m_consoleEngine.newQObject(new DeviceContext{m_consoleEngine});
3,501 ✔
91
    m_consoleEngine.globalObject().setProperty("Device", device);
3,501 ✔
92
    auto names = m_consoleEngine.newQObject(new ScriptableNames{nullptr});
3,501 ✔
93
    edit->setNamespaces(ScriptableNames::makeNamespaces(m_consoleEngine, device, names));
3,501 ✔
94
  }
3,501 ✔
95
  m_consoleEngine.globalObject().setProperty("View", m_consoleEngine.newQObject(new JsViewContext));
3,501 ✔
96
  connect(&m_consoleEngine, &QQmlEngine::exit, this, [&](int retCode) {
3,592 ✔
97
    for(auto& doc : score::GUIAppContext().docManager.documents())
183 ✔
98
      doc->commandStack().markCurrentIndexAsSaved();
92 ✔
99
    // quit() is exit(0), which discarded the code Qt.exit() was given: a script
100
    // could stop the app but never report that it had failed.
101
    qApp->exit(retCode);
91 ✔
102
    QTimer::singleShot(
91 ✔
UNCOV
103
        500, [] { score::GUIApplicationInterface::instance().forceExit(); });
×
104
  });
91 ✔
105
  m_asioContext = std::make_shared<ossia::net::network_context>();
3,501 ✔
106
  m_processMessages = true;
3,501 ✔
107
  m_consoleEngine.globalObject().setProperty(
7,002 ✔
108
      "Protocols",
3,501 ✔
109
      m_consoleEngine.newQObject(new ossia::qt::qml_protocols{m_asioContext, this}));
3,501 ✔
110
  m_asioThread = std::thread{[this] {
7,002 ✔
111
    ossia::set_thread_name("ossia app asio");
3,501 ✔
112
    while(m_processMessages)
7,002 ✔
113
    {
114
      m_asioContext->run();
3,501 ✔
115
    }
116
  }};
3,501 ✔
117

118
  // For scripts of processes that run in the ui thread:
119
  installLocalPathUrlFix(m_scriptProcessUIEngine);
3,501 ✔
120
  m_scriptProcessUIEngine.globalObject().setProperty(
7,002 ✔
121
      "Util", m_scriptProcessUIEngine.newQObject(new JsUtils));
3,501 ✔
122
  m_scriptProcessUIEngine.globalObject().setProperty(
7,002 ✔
123
      "System", m_scriptProcessUIEngine.newQObject(new JsSystem));
3,501 ✔
124
  m_scriptProcessUIEngine.globalObject().setProperty(
7,002 ✔
125
      "Library", m_scriptProcessUIEngine.newQObject(new JsLibrary));
3,501 ✔
126
  m_scriptProcessUIEngine.globalObject().setProperty(
7,002 ✔
127
      "View", m_scriptProcessUIEngine.newQObject(new JsViewContext));
3,501 ✔
128

129
  // Command-line option parsing
130
  QCommandLineParser parser;
3,501 ✔
131

132
  QCommandLineOption script_opt(
3,501 ✔
133
      "script", QCoreApplication::translate("js", "script"), "Script", "");
3,501 ✔
134
  parser.addOption(script_opt);
3,501 ✔
135

136
  parser.parse(ctx.applicationSettings.arguments);
3,501 ✔
137
  for(const QString& script : parser.values(script_opt))
3,605 ✔
138
  {
139
    if(script.isEmpty())
104 ✔
UNCOV
140
      continue;
×
141

142
    if(stringIsScript(script))
104 ✔
143
    {
144
      this->m_start_scripts.push_back(StartScript{.source = script});
3 ✔
145
      continue;
146
    }
147

148
    QFile f{script};
101 ✔
149
    if(!f.open(QIODevice::ReadOnly))
101 ✔
150
    {
151
      qCritical() << "--script: cannot open" << script << ":" << f.errorString();
1 ✔
152
      this->m_start_script_failed = true;
1 ✔
153
      continue;
1 ✔
154
    }
155

156
    const QFileInfo fi{f};
100 ✔
157
    StartScript s;
100 ✔
158
    s.name = script;
100 ✔
159
    s.file = fi.canonicalFilePath();
100 ✔
160
    s.dir = fi.canonicalPath();
100 ✔
161
    // .mjs is what the rest of score calls an ES module (see the library's
162
    // ModuleLibraryHandler); only those go through importModule().
163
    s.module = fi.suffix().compare(QStringLiteral("mjs"), Qt::CaseInsensitive) == 0;
100 ✔
164
    if(!s.module)
100 ✔
165
      s.source = QString::fromUtf8(f.readAll());
96 ✔
166

167
    this->m_start_scripts.push_back(std::move(s));
100 ✔
168
  }
101 ✔
UNCOV
169
}
×
170

171
void ApplicationPlugin::registerCommandHandler(
3 ✔
172
    const QString& name, const QJSValue& fn, QJSEngine* engine)
173
{
174
  if(!fn.isCallable())
3 ✔
175
  {
UNCOV
176
    qWarning() << "Score.registerCommandHandler:" << name << "is not a function";
×
UNCOV
177
    return;
×
178
  }
179
  m_commandHandlers.insert(name, {fn, engine ? engine : &m_consoleEngine});
3 ✔
180
  m_reportedMissingHandlers.remove(name);
3 ✔
181
}
3 ✔
182

183
void ApplicationPlugin::callCommandHandler(
22 ✔
184
    const QString& name, const QByteArray& payload)
185
{
186
  auto it = m_commandHandlers.find(name);
22 ✔
187
  // The script which registered it is gone along with its engine
188
  if(it != m_commandHandlers.end() && !it->engine)
22 ✔
189
  {
190
    m_commandHandlers.erase(it);
×
UNCOV
191
    it = m_commandHandlers.end();
×
UNCOV
192
  }
×
193
  if(it == m_commandHandlers.end())
22 ✔
194
  {
195
    // A backup restored before the script that owns the handler has loaded,
196
    // or a handler that was renamed: the edit cannot be replayed. Say so once
197
    // per name rather than on every undo.
198
    if(!m_reportedMissingHandlers.contains(name))
4 ✔
199
    {
200
      m_reportedMissingHandlers.insert(name);
1 ✔
201
      qWarning() << "ScriptCommand: no handler registered for" << name
2 ✔
202
                 << "- the edit was not replayed";
1 ✔
203
    }
1 ✔
204
    return;
4 ✔
205
  }
206

207
  QJSValue arg;
18 ✔
208
  if(!payload.isEmpty())
18 ✔
209
  {
210
    QJsonParseError err{};
16 ✔
211
    const auto doc = QJsonDocument::fromJson(payload, &err);
212
    if(err.error != QJsonParseError::NoError || !doc.isArray() || doc.array().size() != 1)
16 ✔
213
    {
214
      qWarning() << "ScriptCommand: payload for" << name
×
UNCOV
215
                 << "is not valid:" << err.errorString();
×
UNCOV
216
      return;
×
217
    }
218
    arg = JS::variantToJs(*it->engine, doc.array().first().toVariant());
219
  }
16 ✔
220
  // A copy: the handler may register or replace handlers
221
  auto fn = it->fn;
18 ✔
222
  auto res = fn.call({arg});
18 ✔
223
  if(res.isError())
18 ✔
224
    qWarning() << "ScriptCommand: handler" << name << "failed:" << res.toString();
4 ✔
225
}
22 ✔
226

227
QJSValue ApplicationPlugin::importModule(QQmlEngine& engine, const QString& path)
4 ✔
228
{
229
  QJSValue mod = engine.importModule(path);
4 ✔
230
  if(mod.isError())
4 ✔
231
    return mod;
1 ✔
232

233
  if(auto init = mod.property("initialize"); init.isCallable())
5 ✔
234
    if(const auto res = init.call(); res.isError())
4 ✔
235
      return res;
1 ✔
236

237
  engine.globalObject().setProperty(QFileInfo{path}.baseName(), mod);
2 ✔
238
  return mod;
2 ✔
239
}
4 ✔
240

241
void ApplicationPlugin::on_newDocument(score::Document& doc)
1,297 ✔
242
{
243
  score::addDocumentPlugin<DocumentPlugin>(doc);
1,297 ✔
244
}
1,297 ✔
245

246
ApplicationPlugin::~ApplicationPlugin()
7,000 ✔
247
{
3,500 ✔
248
  m_processMessages = false;
3,500 ✔
249
  m_asioContext->context.stop();
3,500 ✔
250
  m_asioThread.join();
3,500 ✔
251
}
7,000 ✔
252

253
void ApplicationPlugin::on_createdDocument(score::Document& doc)
1,365 ✔
254
{
255
  // Local Tree
256
  LocalTree::DocumentPlugin* lt = doc.context().findPlugin<LocalTree::DocumentPlugin>();
1,365 ✔
257
  if(lt)
1,365 ✔
258
  {
259
    auto& root = lt->device().get_root_node();
1,365 ✔
260

261
    auto node = root.create_child("script");
1,365 ✔
262
    auto address = node->create_parameter(ossia::val_type::STRING);
1,365 ✔
263
    address->set_value(std::string{});
1,365 ✔
264
    address->set_access(ossia::access_mode::SET);
1,365 ✔
265
    address->add_callback([&](const ossia::value& v) {
1,395 ✔
266
      ossia::qt::run_async(
30 ✔
267
          this, [this, str = QString::fromStdString(ossia::convert<std::string>(v))] {
60 ✔
268
        auto res = m_consoleEngine.evaluate(str);
30 ✔
269
        if(res.isError())
30 ✔
270
        {
271
          qDebug() << res.toString();
20 ✔
272
        }
20 ✔
273
      });
30 ✔
274
    });
30 ✔
275
  }
1,365 ✔
276

277
  // Custom data
278
  if(auto customData = doc.context().findPlugin<DocumentPlugin>(); !customData)
1,365 ✔
279
    score::addDocumentPlugin<DocumentPlugin>(doc);
1 ✔
280

281
  if(m_start_script_failed)
1,365 ✔
282
  {
283
    qGuiApp->exit(2);
1 ✔
284
    return;
1 ✔
285
  }
286

287
}
1,365 ✔
288

289
void ApplicationPlugin::runStartScripts()
88 ✔
290
{
291
  for(const StartScript& s : m_start_scripts)
173 ✔
292
  {
293
    if(!s.dir.isEmpty())
89 ✔
294
      m_consoleEngine.addImportPath(s.dir);
87 ✔
295

296
    // Report a throwing --script: an unresolvable readFile returns an empty
297
    // string and eval("") is a no-op, so the process would otherwise exit
298
    // reporting success and a harness could not tell that from a pass.
299
    const auto res = s.module ? importModule(m_consoleEngine, s.file)
89 ✔
300
                              : m_consoleEngine.evaluate(s.source, s.name);
85 ✔
301
    if(res.isError())
89 ✔
302
    {
303
      qCritical().noquote()
8 ✔
304
          << "--script:" << (s.name.isEmpty() ? QStringLiteral("<inline>") : s.name)
4 ✔
305
          << "line" << res.property("lineNumber").toInt() << ":" << res.toString();
4 ✔
306
      qGuiApp->exit(3);
4 ✔
307
      return;
4 ✔
308
    }
309
  }
89 ✔
310
}
88 ✔
311
void ApplicationPlugin::initialize()
3,501 ✔
312
{
313
  // Documents named on the command line are loaded before afterStartup(), and
314
  // compiling one of their scripts is what makes the library's QML modules
315
  // needed: the import paths have to be in place by then.
316
  for(auto& p : this->context.settings<Library::Settings::Model>().getIncludePaths())
3,501 ✔
317
  {
UNCOV
318
    m_scriptProcessUIEngine.addImportPath(p);
×
319
    // The console engine runs --script, the console panel and every library
320
    // .mjs; without this they could not import what a JS process can, which
321
    // made the same `import` line work in a process and fail in a script.
UNCOV
322
    m_consoleEngine.addImportPath(p);
×
323
  }
324
}
3,501 ✔
325

326
void ApplicationPlugin::afterStartup()
102 ✔
327
{
328
  if(!m_start_scripts.empty())
102 ✔
329
  {
330
    QTimer::singleShot(
101 ✔
331
        (1 + context.applicationSettings.waitAfterLoad) * 1000, this,
101 ✔
332
        [this] { runStartScripts(); });
189 ✔
333
  }
101 ✔
334

335
#if __has_include(<QQuickWindow>)
336
  if(QFileInfo f{context.applicationSettings.ui}; f.isFile())
106 ✔
337
  {
338
    m_comp = new QQmlComponent{&m_consoleEngine, f.absoluteFilePath(), this};
4 ✔
339

340
    if(auto obj = m_comp->create())
4 ✔
341
    {
342
      if(auto item = qobject_cast<QQuickItem*>(obj))
1 ✔
343
      {
UNCOV
344
        m_window = new QQuickWindow{};
×
345
  // QWidget gets these from QWidgetPrivate::adjustFlags; a bare QQuickWindow
346
  // does not, and on platforms where Qt draws the chrome itself (wasm) that
347
  // leaves the window with no title bar, close or minimise button.
348
  m_window->setFlags(
×
349
      m_window->flags() | Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
×
UNCOV
350
      | Qt::WindowCloseButtonHint | Qt::WindowMinimizeButtonHint
×
UNCOV
351
      | Qt::WindowMaximizeButtonHint);
×
352
#if defined(__EMSCRIPTEN__)
353
  // Qt for wasm reports ShowIsFullScreen unconditionally, so QWindow::show()
354
  // turns into showFullScreen() for every top level: the requested size is
355
  // discarded and the full-screen state suppresses the frame. Only Qt::Dialog
356
  // and Qt::Popup opt out (QWasmIntegration::defaultWindowState).
357
  m_window->setFlags(m_window->flags() | Qt::Dialog);
358
#endif
359
        m_window->setWidth(640);
×
UNCOV
360
        m_window->setHeight(480);
×
UNCOV
361
        item->setParentItem(m_window->contentItem());
×
362
#if defined(__EMSCRIPTEN__)
363
        m_window->showNormal();
364
#else
365
        m_window->show();
×
366
#endif
UNCOV
367
        return;
×
368
      }
369
    }
1 ✔
370
    else
371
    {
372
      qDebug() << m_comp->errorString();
3 ✔
373
      qGuiApp->exit(1);
3 ✔
374
    }
375
    delete m_comp;
4 ✔
376
  }
4 ✔
377
#endif
378
}
102 ✔
379
}
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