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

ossia / score / 30213925900

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

push

github

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

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

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

30400 of 198713 relevant lines covered (15.3%)

997.67 hits per line

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

0.0
/src/plugins/score-plugin-gfx/Gfx/Graph/Window.cpp
1
#include <Gfx/Graph/Window.hpp>
2
#include <Gfx/Graph/Utils.hpp>
3
#include <Gfx/Settings/Model.hpp>
4

5
#include <score/application/ApplicationContext.hpp>
6
#include <score/application/GUIApplicationContext.hpp>
7
#include <score/gfx/Vulkan.hpp>
8

9
#include <core/application/ApplicationInterface.hpp>
10

11
#include <QGuiApplication>
12
#include <QPointer>
13
#include <QStringList>
14

15
#include <algorithm>
16

17
#if defined(__EMSCRIPTEN__)
18
#include <emscripten/em_asm.h>
19
#include <emscripten/emscripten.h>
20
#include <emscripten/val.h>
21

22
#include <string>
23
#endif
24
#include <QPlatformSurfaceEvent>
25
#include <QTimer>
26
#include <QtGui/private/qrhigles2_p.h>
27
#if QT_HAS_VULKAN
28
#if __has_include(<QtGui/private/qrhivulkan_p.h>)
29
#include <QtGui/private/qrhivulkan_p.h>
30
#else
31
#undef QT_HAS_VULKAN
32
#endif
33
#endif
34
#include <wobjectimpl.h>
35

36
W_OBJECT_IMPL(score::gfx::Window)
×
37
namespace score::gfx
38
{
39

40
Window::Window(GraphicsApi graphicsApi)
×
41
    : m_api{graphicsApi}
×
42
{
×
43
  setCursor(Qt::BlankCursor);
×
44

45
#if defined(__EMSCRIPTEN__)
46
  // No OS window manager on wasm: without this the output window drops behind
47
  // the main window when it's activated and can't be brought back.
48
  setFlag(Qt::WindowStaysOnTopHint, true);
49
  // See the note in the JS plugin: without this the output opens full-screen at
50
  // the viewport size, with no frame to close or move it by.
51
  setFlag(Qt::Dialog, true);
52

53
  // QWasmWindowTreeNode::onSubtreeChanged() activates every window inserted in
54
  // the tree unless it is a tooltip, a popup, or carries this property.
55
  setProperty("_q_showWithoutActivating", true);
56
#endif
57

58
  QSurfaceFormat fmt = QSurfaceFormat::defaultFormat();
×
59

60
  // Tell the platform plugin what we want.
61
  switch(m_api)
×
62
  {
×
63
    default:
64
    case OpenGL:
65
#if QT_CONFIG(opengl)
66
      setSurfaceType(OpenGLSurface);
×
67
#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0)
68
      fmt = QRhiGles2InitParams::adjustedFormat();
69
#endif
70
#endif
71
    break;
×
72

73
#if QT_HAS_VULKAN
74
    case Vulkan:
75
      setSurfaceType(VulkanSurface);
×
76
      setVulkanInstance(score::gfx::staticVulkanInstance());
×
77
      break;
×
78
#endif
79

80
#if defined(_WIN32)
81
    case D3D11:
82
    case D3D12:
83
      setSurfaceType(Direct3DSurface);
84
      break;
85
#endif
86

87
#if defined(__APPLE__)
88
    case Metal:
89
      setSurfaceType(MetalSurface);
90
      break;
91
#endif
92
  }
93

94
  const auto& settings = score::AppContext().settings<Gfx::Settings::Model>();
×
95
  fmt.setSwapInterval(settings.getVSync() ? 1 : 0);
×
96

97
  switch(settings.getBuffers())
×
98
  {
×
99
    default:
100
    case 1:
101
      fmt.setSwapBehavior(QSurfaceFormat::SwapBehavior::SingleBuffer);
×
102
      break;
×
103
    case 2:
104
      fmt.setSwapBehavior(QSurfaceFormat::SwapBehavior::DoubleBuffer);
×
105
      break;
×
106
    case 3:
107
      fmt.setSwapBehavior(QSurfaceFormat::SwapBehavior::TripleBuffer);
×
108
      break;
×
109
  }
110

111
  const int samples = settings.resolveSamples(m_api);
×
112
  fmt.setSamples(samples);
×
113

114
  setFormat(fmt);
×
115

116
  if(auto platform = qGuiApp->platformName();
×
117
     platform.contains("eglfs") || platform.contains("vkkhr"))
×
118
    m_embeddedFullscreen = true;
×
119
}
×
120

121
Window::~Window()
×
122
{
×
123
  m_closed = true;
×
124
}
×
125

126
void Window::init()
×
127
{
128
  onWindowReady();
×
129
}
×
130

131
void Window::resizeSwapChain()
×
132
{
133
  if(m_swapChain)
×
134
  {
135
    const QSize surface = m_swapChain->surfacePixelSize();
×
136

137
    // QGles2SwapChain::createOrResize() returns true unconditionally, even for
138
    // an empty surface: it is not a usable "the swapchain is ready" signal.
139
    // Building the render list against an empty swapchain gives every node a
140
    // 1x1 render target (QRhi clamps empty texture sizes up) and a 0x0
141
    // viewport in the final blit, i.e. a permanently black window, since
142
    // nothing rebuilds the render list unless the surface size changes again.
143
    if(surface.isEmpty())
×
144
    {
145
      m_hasSwapChain = false;
×
146
      m_newlyExposed = true;
×
147
      scheduleRetry();
×
148
      return;
×
149
    }
150

151
    m_hasSwapChain = m_swapChain->createOrResize();
×
152
    if(state)
×
153
      state->outputSize = m_swapChain->currentPixelSize();
×
154

155

156
    if(onResize)
×
157
      onResize();
×
158
  }
×
159
  else
160
  {
161
    m_hasSwapChain = false;
×
162
  }
163
}
×
164

165
void Window::releaseSwapChain()
×
166
{
167
  if(m_swapChain && m_hasSwapChain)
×
168
  {
169
    m_hasSwapChain = false;
×
170
    m_swapChain->destroy();
×
171

172
    // The render list is built against this swapchain: force a full rebuild
173
    // when the window comes back rather than reusing it as-is.
174
    m_newlyExposed = true;
×
175
  }
×
176
}
×
177

178

179
void Window::scheduleRetry()
×
180
{
181
  // Retry, but not through requestUpdate(). These paths are the ones taken when
182
  // the window is not ready or the frame failed, and their condition can be
183
  // permanent (never exposed, zero-sized surface, a swapchain that will not
184
  // resize): re-arming a frame callback from inside a frame callback then burns
185
  // a 60Hz rAF forever and, because each rAF callback's stack is chained to the
186
  // one that scheduled it, builds an unbounded async stack. Any warning logged
187
  // from such a loop then carries the whole chain, which is what made DevTools
188
  // unusable while diagnosing exactly these paths. A timer starts a fresh stack
189
  // and retries ten times a second, which is plenty for "wait until the canvas
190
  // has a size".
191
  if(m_retryScheduled)
×
192
    return;
×
193

194
  m_retryScheduled = true;
×
195
  QTimer::singleShot(retry_interval_ms, this, [this] {
×
196
    m_retryScheduled = false;
×
197
    render();
×
198
  });
×
199
}
×
200

201
void Window::handleDeviceLost()
×
202
{
203
  if(m_deviceLost)
×
204
    return;
×
205

206
  m_deviceLost = true;
×
207
  m_hasSwapChain = false;
×
208
  m_canRender = false;
×
209

210
  // "QRhiGles2: Context is lost." is also emitted, benignly, whenever a QRhi is
211
  // destroyed -- ScreenNode::destroyOutput() -> RenderState::destroy() ->
212
  // ~QRhi() -> QRhiGles2::destroy() -> ensureContext(). That path never reaches
213
  // here (destroyOutput() clears m_swapChain first, so render() returns before
214
  // beginFrame), and neither does application shutdown, but say nothing on the
215
  // way out regardless: only a context that dies under a *live* window is a
216
  // defect worth reporting.
217
  if(m_closed || QCoreApplication::closingDown())
×
218
    return;
×
219

220
  qCritical() << "score::gfx::Window: the graphics context was lost while the output "
×
221
                 "was live. This output has stopped rendering.";
222

223
  if(onDeviceLost)
×
224
  {
225
    // Deferred, and not through this window: the handler is expected to
226
    // destroy and rebuild the output, i.e. to delete this.
227
    QPointer<Window> self{this};
×
228
    QMetaObject::invokeMethod(
×
229
        qApp,
×
230
        [self, cb = onDeviceLost] {
×
231
      if(self)
×
232
        cb();
×
233
    },
×
234
        Qt::QueuedConnection);
235
  }
×
236
}
×
237

238
bool Window::checkDeviceLost(int frameOpResult)
×
239
{
240
  if(frameOpResult != QRhi::FrameOpDeviceLost
×
241
     && !(state && state->rhi && state->rhi->isDeviceLost()))
×
242
    return false;
×
243

244
  handleDeviceLost();
×
245
  return true;
×
246
}
×
247

248
void Window::render()
×
249
{
250
  static constexpr double fps_smoothing = .8;
251
  if(m_closed)
×
252
    return;
×
253

254
  // A lost context never comes back on its own: without this the window would
255
  // call beginFrame() on a dead QRhi on every update request and log
256
  // "QRhiGles2: Context is lost." forever.
257
  if(m_deviceLost)
×
258
    return;
×
259

260
  if(onUpdate)
×
261
  {
262
    onUpdate();
×
263
  }
×
264

265
  if(!m_swapChain)
×
266
    return;
×
267

268
  if(!m_hasSwapChain || m_notExposed)
×
269
  {
270
    // wasm delivers a one-shot expose (QWasmWindow::setVisible), so if the
271
    // surface had no size when exposeEvent latched m_notExposed, nothing ever
272
    // clears it again and the window stays black. Recover once it has a size.
273
    if(isExposed() && m_swapChain && !m_swapChain->surfacePixelSize().isEmpty())
×
274
    {
275
      m_notExposed = false;
×
276
      m_newlyExposed = true;
×
277
      // fall through: the resize block below will (re)create the swapchain
278
    }
×
279
    else
280
    {
281
      scheduleRetry();
×
282
      return;
×
283
    }
284
  }
×
285

286
  if(m_swapChain->currentPixelSize() != m_swapChain->surfacePixelSize()
×
287
     || m_newlyExposed)
×
288
  {
289
    resizeSwapChain();
×
290
    if(!m_hasSwapChain)
×
291
    {
292
      scheduleRetry();
×
293
      return;
×
294
    }
295
    m_newlyExposed = false;
×
296
  }
×
297

298
  if(m_canRender && state)
×
299
  {
300
    QRhi::FrameOpResult r = state->rhi->beginFrame(m_swapChain, {});
×
301
    if(checkDeviceLost(r))
×
302
      return;
×
303
    if(r == QRhi::FrameOpSwapChainOutOfDate)
×
304
    {
305
      resizeSwapChain();
×
306
      if(!m_hasSwapChain)
×
307
      {
308
        scheduleRetry();
×
309
        return;
×
310
      }
311
      r = state->rhi->beginFrame(m_swapChain);
×
312
      if(checkDeviceLost(r))
×
313
        return;
×
314
    }
×
315
    if(r != QRhi::FrameOpSuccess)
×
316
    {
317
      scheduleRetry();
×
318
      return;
×
319
    }
320

321
    const auto commands = m_swapChain->currentFrameCommandBuffer();
×
322
    onRender(*commands);
×
323

324
    state->rhi->endFrame(m_swapChain, {});
×
325
    {
326
      // 1. Calculate the time elapsed since the last frame
327
      if(const auto frame_ns = m_timer.nsecsElapsed(); frame_ns > 0)
×
328
      {
329
        const double fps = 1e9 / frame_ns;
×
330

331
        // 2. Smooth things a bit
332
        if(m_fps == 0.0f)
×
333
          m_fps = fps;
×
334
        else
335
          m_fps = (fps * fps_smoothing) + (m_fps * (1.0f - fps_smoothing));
×
336
      }
×
337
      m_timer.restart();
×
338
    }
339
  }
×
340
  else
341
  {
342
    QRhi::FrameOpResult r = state->rhi->beginFrame(m_swapChain, {});
×
343
    if(checkDeviceLost(r))
×
344
      return;
×
345
    if(r == QRhi::FrameOpSwapChainOutOfDate)
×
346
    {
347
      resizeSwapChain();
×
348
      if(!m_hasSwapChain)
×
349
      {
350
        scheduleRetry();
×
351
        return;
×
352
      }
353
      r = state->rhi->beginFrame(m_swapChain);
×
354
      if(checkDeviceLost(r))
×
355
        return;
×
356
    }
×
357
    if(r != QRhi::FrameOpSuccess)
×
358
    {
359
      scheduleRetry();
×
360
      return;
×
361
    }
362

363
    auto buf = m_swapChain->currentFrameCommandBuffer();
×
364
    auto batch = state->rhi->nextResourceUpdateBatch();
×
365
    buf->beginPass(m_swapChain->currentFrameRenderTarget(), Qt::black, {1.0f, 0}, batch);
×
366
    buf->endPass();
×
367

368
    state->rhi->endFrame(m_swapChain, {});
×
369
    m_fps = 0.;
×
370
  }
371

372
  if(m_fpsPushTimer.elapsed() > 50)
×
373
  {
374
    fps(m_fps);
×
375
    m_fpsPushTimer.restart();
×
376
  }
×
377

378
  if(this->onUpdate) {
×
379
    // requestUpdate is only to be used in the vsync case
380
    requestUpdate();
×
381
  }
×
382
}
×
383

384
void Window::exposeEvent(QExposeEvent* ev)
×
385
{
386
  if(!onWindowReady)
×
387
  {
388
    return;
×
389
  }
390

391
  if(isExposed() && !m_running)
×
392
  {
393
    m_running = true;
×
394
    init();
×
395
    resizeSwapChain();
×
396
  }
×
397

398
  if(m_hasSwapChain && !m_swapChain)
×
399
  {
400
    qDebug("exposeEvent: m_hasSwapChain && !m_swapChain");
×
401
    m_hasSwapChain = false;
×
402
  }
×
403

404
  const QSize surfaceSize = m_hasSwapChain ? m_swapChain->surfacePixelSize() : QSize();
×
405

406
  if((!isExposed() || (m_hasSwapChain && surfaceSize.isEmpty())) && m_running)
×
407
    m_notExposed = true;
×
408

409
  if(isExposed() && m_running && m_notExposed && !surfaceSize.isEmpty())
×
410
  {
411
    m_notExposed = false;
×
412
    m_newlyExposed = true;
×
413
  }
×
414

415
  if(isExposed())
×
416
  {
417
    m_closed = false;
×
418
  }
×
419

420
  if(isExposed() && !surfaceSize.isEmpty())
×
421
  {
422
    m_timer.restart();
×
423
    m_fpsPushTimer.restart();
×
424
    render();
×
425
  }
×
426
}
×
427

428
void Window::mouseDoubleClickEvent(QMouseEvent* ev)
×
429
{
430
  setWindowStates(windowStates() ^ Qt::WindowFullScreen);
×
431
}
×
432

433
bool Window::event(QEvent* e)
×
434
{
435
  switch(e->type())
×
436
  {
437
    case QEvent::UpdateRequest:
438
      render();
×
439
      break;
×
440

441
    case QEvent::TabletMove: {
442
      auto ev = static_cast<QTabletEvent*>(e);
×
443
      this->tabletMove(ev);
×
444
      this->interactiveEvent(e);
×
445
      break;
×
446
    }
447
    case QEvent::TabletPress:
448
    case QEvent::TabletRelease:
449
      this->interactiveEvent(e);
×
450
      break;
×
451

452
    case QEvent::MouseButtonPress:
453
    case QEvent::MouseButtonRelease:
454
    case QEvent::MouseButtonDblClick:
455
      this->interactiveEvent(e);
×
456
      break;
×
457

458
    case QEvent::MouseMove: {
459
      auto ev = static_cast<QMouseEvent*>(e);
×
460
      this->mouseMove(ev->globalPosition(), ev->scenePosition());
×
461
      this->interactiveEvent(e);
×
462
      break;
×
463
    }
464
    case QEvent::KeyPress: {
465
      auto ev = static_cast<QKeyEvent*>(e);
×
466
      if(!ev->isAutoRepeat())
×
467
      {
468
        this->key(ev->key(), ev->text());
×
469
        this->interactiveEvent(e);
×
470
        if(ev->key() == Qt::Key_Escape)
×
471
          if(m_embeddedFullscreen)
×
472
            QMetaObject::invokeMethod(
×
473
                qGuiApp, [] { score::GUIApplicationInterface::instance().forceExit(); });
×
474
      }
×
475

476
      break;
×
477
    }
478
    case QEvent::KeyRelease: {
479
      auto ev = static_cast<QKeyEvent*>(e);
×
480
      if(!ev->isAutoRepeat())
×
481
      {
482
        this->keyRelease(ev->key(), ev->text());
×
483
        this->interactiveEvent(e);
×
484
      }
×
485
      break;
×
486
    }
487
    case QEvent::PlatformSurface:
488
      if(static_cast<QPlatformSurfaceEvent*>(e)->surfaceEventType()
×
489
         == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) // fallthrough
×
490
      case QEvent::Close: {
491
        releaseSwapChain();
×
492
        m_running = false;
×
493
        m_hasSwapChain = false;
×
494
        m_notExposed = true;
×
495
        m_closed = true;
×
496
        if(onClose)
×
497
          onClose();
×
498
#if defined(__EMSCRIPTEN__)
499
        score::reclaimMainWindowFocus();
500
#endif
501
      }
×
502
      break;
×
503

504
#if defined(__EMSCRIPTEN__)
505
    case QEvent::Hide:
506
      score::reclaimMainWindowFocus();
507
      break;
508
#endif
509

510
      default:
511
        break;
×
512
  }
513

514
  return QWindow::event(e);
×
515
}
×
516

517
}
518

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