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

ossia / score / 30157002862

25 Jul 2026 11:53AM UTC coverage: 15.406% (+0.1%) from 15.311%
30157002862

Pull #2119

github

web-flow
Merge 51e0e1b89 into 13afd939a
Pull Request #2119: gfx: caching + offscreen infrastructure (pre-scene, from #2109)

247 of 597 new or added lines in 12 files covered. (41.37%)

9 existing lines in 4 files now uncovered.

30668 of 199059 relevant lines covered (15.41%)

981.72 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/RenderClock.cpp
1
#include <Gfx/Graph/OutputNode.hpp>
2
#include <Gfx/Graph/RenderClock.hpp>
3

4
#include <score/tools/Timers.hpp>
5

6
#include <QCoreApplication>
7
#include <QEventLoop>
8
#include <QThread>
9

10
#include <algorithm>
11
#include <chrono>
12

13
namespace score::gfx
14
{
15

NEW
16
RenderClock::~RenderClock() = default;
×
17

18
// ---------------------------------------------------------------------------
19
// TimerClock
20
// ---------------------------------------------------------------------------
21

NEW
22
TimerClock::TimerClock(score::Timers& timers, QObject* owner, double frequencyHz)
×
NEW
23
    : m_timers{timers}
×
NEW
24
    , m_owner{owner}
×
NEW
25
    , m_frequency{frequencyHz}
×
NEW
26
{
×
NEW
27
}
×
28

NEW
29
TimerClock::~TimerClock()
×
NEW
30
{
×
NEW
31
  stop();
×
NEW
32
}
×
33

NEW
34
void TimerClock::addOutput(OutputNode* o)
×
35
{
NEW
36
  if(o && std::find(m_outputs.begin(), m_outputs.end(), o) == m_outputs.end())
×
NEW
37
    m_outputs.push_back(o);
×
NEW
38
}
×
39

NEW
40
void TimerClock::removeOutput(OutputNode* o)
×
41
{
NEW
42
  std::erase(m_outputs, o);
×
NEW
43
}
×
44

NEW
45
void TimerClock::start(std::function<void()> tick)
×
46
{
NEW
47
  m_tick = std::move(tick);
×
NEW
48
  if(!m_timer)
×
NEW
49
    m_timer = m_timers.acquireTimer(m_owner, m_frequency);
×
50

51
  // Queued connection delivered on the render thread (m_receiver lives there,
52
  // same as m_owner). m_receiver — not m_owner — is the receiver context so
53
  // that destroying this TimerClock purges any pending timeout events: with
54
  // m_owner (which outlives us) as context, a queued tick already posted before
55
  // stop()'s disconnect() would still fire the lambda on our freed `this`
56
  // (heap-use-after-free at shutdown).
NEW
57
  m_conn = QObject::connect(
×
NEW
58
      m_timer, &score::HighResolutionTimer::timeout, &m_receiver,
×
NEW
59
      [this](score::HighResolutionTimer*) {
×
NEW
60
        if(m_tick)
×
NEW
61
          m_tick();
×
NEW
62
      },
×
63
      Qt::QueuedConnection);
NEW
64
}
×
65

NEW
66
void TimerClock::stop()
×
67
{
NEW
68
  if(m_conn)
×
69
  {
NEW
70
    QObject::disconnect(m_conn);
×
NEW
71
    m_conn = {};
×
NEW
72
  }
×
NEW
73
  if(m_timer)
×
74
  {
NEW
75
    m_timers.releaseTimer(m_owner, m_timer);
×
NEW
76
    m_timer = nullptr;
×
NEW
77
  }
×
NEW
78
  m_tick = {};
×
NEW
79
}
×
80

81
// ---------------------------------------------------------------------------
82
// DisplayVSyncClock
83
// ---------------------------------------------------------------------------
84

NEW
85
DisplayVSyncClock::DisplayVSyncClock(OutputNode& output)
×
NEW
86
    : m_output{output}
×
NEW
87
{
×
NEW
88
}
×
89

NEW
90
DisplayVSyncClock::~DisplayVSyncClock()
×
NEW
91
{
×
NEW
92
  stop();
×
NEW
93
}
×
94

NEW
95
void DisplayVSyncClock::start(std::function<void()> tick)
×
96
{
NEW
97
  m_output.setVSyncCallback(std::move(tick));
×
NEW
98
}
×
99

NEW
100
void DisplayVSyncClock::stop()
×
101
{
NEW
102
  m_output.setVSyncCallback({});
×
NEW
103
}
×
104

105
// ---------------------------------------------------------------------------
106
// ExternalGenlockClock
107
// ---------------------------------------------------------------------------
108

NEW
109
ExternalGenlockClock::ExternalGenlockClock(
×
110
    QObject* owner, std::function<bool()> waitForNextTick)
NEW
111
    : m_owner{owner}
×
NEW
112
    , m_waitForNextTick{std::move(waitForNextTick)}
×
NEW
113
{
×
114
}
115

NEW
116
ExternalGenlockClock::~ExternalGenlockClock()
×
NEW
117
{
×
NEW
118
  stop();
×
NEW
119
}
×
120

NEW
121
void ExternalGenlockClock::start(std::function<void()> tick)
×
122
{
NEW
123
  if(m_running.exchange(true))
×
NEW
124
    return;
×
NEW
125
  m_tick = std::move(tick);
×
NEW
126
  m_exited.store(false, std::memory_order_release);
×
NEW
127
  m_thread = std::thread{[this] { run(); }};
×
NEW
128
}
×
129

NEW
130
void ExternalGenlockClock::run()
×
131
{
NEW
132
  while(m_running.load(std::memory_order_relaxed))
×
133
  {
134
    // Pull facet: block on the hardware tick (card VBI). false => timeout; loop
135
    // and re-check running so stop() stays responsive.
NEW
136
    if(m_waitForNextTick && !m_waitForNextTick())
×
NEW
137
      continue;
×
NEW
138
    if(!m_running.load(std::memory_order_relaxed))
×
NEW
139
      break;
×
140

141
    // Marshal the render onto the owner's (render) thread and BLOCK until it
142
    // returns: the next VBI wait then starts only after this frame is rendered
143
    // + pushed, so render can never run ahead of the card's genlock. Same
144
    // cross-thread hand-off PacedFramePump uses for the submit side.
NEW
145
    QMetaObject::invokeMethod(
×
NEW
146
        m_owner, [this] { if(m_tick) m_tick(); },
×
147
        Qt::BlockingQueuedConnection);
148
  }
NEW
149
  m_exited.store(true, std::memory_order_release);
×
NEW
150
}
×
151

NEW
152
void ExternalGenlockClock::stop()
×
153
{
NEW
154
  if(!m_running.exchange(false))
×
NEW
155
    return;
×
156

NEW
157
  if(m_thread.joinable())
×
158
  {
159
    // The tick thread may be parked in the BlockingQueuedConnection invoke,
160
    // waiting for m_owner's event loop to run the functor. If stop() runs on
161
    // the owner thread (the usual teardown case, after the event loop quits) a
162
    // bare join() would dead-lock. Pump the owner's event queue until the tick
163
    // thread has drained any in-flight blocking invoke and left its loop.
NEW
164
    const bool onOwnerThread
×
NEW
165
        = m_owner && QThread::currentThread() == m_owner->thread();
×
NEW
166
    while(!m_exited.load(std::memory_order_acquire))
×
167
    {
NEW
168
      if(onOwnerThread)
×
NEW
169
        QCoreApplication::processEvents(QEventLoop::AllEvents, 2);
×
170
      else
NEW
171
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
×
172
    }
NEW
173
    m_thread.join();
×
NEW
174
  }
×
NEW
175
  m_tick = {};
×
NEW
176
}
×
177

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