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

mcallegari / qlcplus / 22149820832

18 Feb 2026 05:11PM UTC coverage: 34.067% (+0.08%) from 33.99%
22149820832

push

github

mcallegari
engine: improve universe composition efficiency

- Universe::tick() now releases the semaphore only if no pending token exists.
- Elapsed-time aware fading: improves fade timing accuracy under variable CPU load and scheduler jitter; fades progress according to real time
instead of assuming perfect 20 ms cadence
- Reduced lock hold in Universe fader processing to avoid holding the universe fader list lock during potentially expensive per-fader channel writes, reducing contention
and improving scalability with many faders/universes
- Lower allocation/copy pressure in output emission: avoids per-tick temporary copy for plugin output write path while preserving safe ownership semantics for queued
signal consumers
- GenericFader channel lookup/update restructuring: reduces lock churn and repeated channel metadata setup overhead in high-channel scenes
- Locking correctness improvements in GenericFader: fixes data-race risk and ensures thread-safe mutation behavior
- Additional performance validation is done through engine/test/universeperf

122 of 202 new or added lines in 9 files covered. (60.4%)

2 existing lines in 2 files now uncovered.

17708 of 51980 relevant lines covered (34.07%)

41221.91 hits per line

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

81.94
/engine/src/genericfader.cpp
1
/*
2
  Q Light Controller
3
  genericfader.cpp
4

5
  Copyright (c) Heikki Junnila
6

7
  Licensed under the Apache License, Version 2.0 (the "License");
8
  you may not use this file except in compliance with the License.
9
  You may obtain a copy of the License at
10

11
      http://www.apache.org/licenses/LICENSE-2.0.txt
12

13
  Unless required by applicable law or agreed to in writing, software
14
  distributed under the License is distributed on an "AS IS" BASIS,
15
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
  See the License for the specific language governing permissions and
17
  limitations under the License.
18
*/
19

20
#include <QDebug>
21

22
#include "genericfader.h"
23
#include "fadechannel.h"
24
#include "doc.h"
25

26
GenericFader::GenericFader(QObject *parent)
2,552✔
27
    : QObject(parent)
28
    , m_fid(Function::invalidId())
2,552✔
29
    , m_priority(Universe::Auto)
2,552✔
30
    , m_handleSecondary(false)
2,552✔
31
    , m_intensity(1.0)
2,552✔
32
    , m_parentIntensity(1.0)
2,552✔
33
    , m_paused(false)
2,552✔
34
    , m_enabled(true)
2,552✔
35
    , m_fadeOut(false)
2,552✔
36
    , m_deleteRequest(false)
2,552✔
37
    , m_blendMode(Universe::NormalBlend)
2,552✔
38
    , m_monitoring(false)
5,104✔
39
{
40
}
2,552✔
41

42
GenericFader::~GenericFader()
5,102✔
43
{
44
}
5,102✔
45

46
QString GenericFader::name() const
×
47
{
48
    return m_name;
×
49
}
50

51
void GenericFader::setName(QString name)
2,523✔
52
{
53
    m_name = name;
2,523✔
54
}
2,523✔
55

56
quint32 GenericFader::parentFunctionID() const
9✔
57
{
58
    return m_fid;
9✔
59
}
60

61
void GenericFader::setParentFunctionID(quint32 fid)
2,523✔
62
{
63
    m_fid = fid;
2,523✔
64
}
2,523✔
65

66
int GenericFader::priority() const
18,109✔
67
{
68
    return m_priority;
18,109✔
69
}
70

71
void GenericFader::setPriority(int priority)
2,551✔
72
{
73
    m_priority = priority;
2,551✔
74
}
2,551✔
75

76
bool GenericFader::handleSecondary()
1,710,428✔
77
{
78
    return m_handleSecondary;
1,710,428✔
79
}
80

81
void GenericFader::setHandleSecondary(bool enable)
2,523✔
82
{
83
    m_handleSecondary = enable;
2,523✔
84
}
2,523✔
85

86
quint32 GenericFader::channelHash(quint32 fixtureID, quint32 channel)
1,710,469✔
87
{
88
    return ((fixtureID & 0x0000FFFF) << 16) | (channel & 0x0000FFFF);
1,710,469✔
89
}
90

91
void GenericFader::add(const FadeChannel& ch)
17✔
92
{
93
    quint32 hash = channelHash(ch.fixture(), ch.channel());
17✔
94

95
    QWriteLocker l(&m_channelsLock);
17✔
96
    QHash<quint32,FadeChannel>::iterator channelIterator = m_channels.find(hash);
17✔
97
    if (channelIterator != m_channels.end())
17✔
98
    {
99
        // perform a HTP check
100
        if (channelIterator.value().current() <= ch.current())
3✔
101
            channelIterator.value() = ch;
3✔
102
    }
103
    else
104
    {
105
        m_channels.insert(hash, ch);
14✔
106
    }
107
}
17✔
108

109
void GenericFader::replace(const FadeChannel &ch)
1✔
110
{
111
    quint32 hash = channelHash(ch.fixture(), ch.channel());
1✔
112
    QWriteLocker l(&m_channelsLock);
1✔
113
    m_channels.insert(hash, ch);
1✔
114
}
1✔
115

116
void GenericFader::remove(FadeChannel *ch)
2✔
117
{
118
    if (ch == NULL)
2✔
119
        return;
×
120

121
    quint32 hash = channelHash(ch->fixture(), ch->channel());
2✔
122
    QWriteLocker l(&m_channelsLock);
2✔
123
    if (m_channels.remove(hash) == 0)
2✔
124
        qDebug() << "No FadeChannel found with hash" << hash;
1✔
125
}
2✔
126

127
void GenericFader::removeAll()
5✔
128
{
129
    QWriteLocker l(&m_channelsLock);
5✔
130
    m_channels.clear();
5✔
131
    m_channelCache.clear();
5✔
132
}
5✔
133

134
bool GenericFader::deleteRequested()
1,323,101✔
135
{
136
    return m_deleteRequest;
1,323,101✔
137
}
138

139
void GenericFader::requestDelete()
103✔
140
{
141
    m_deleteRequest = true;
103✔
142
}
103✔
143

144
FadeChannel *GenericFader::getChannelFader(const Doc *doc, Universe *universe, quint32 fixtureID, quint32 channel)
794,357✔
145
{
146
    QWriteLocker l(&m_channelsLock);
794,357✔
147
    return &channelFaderLocked(doc, universe, fixtureID, channel);
1,588,714✔
148
}
794,357✔
149

150
QHash<quint32, FadeChannel> GenericFader::channels() const
74✔
151
{
152
    QReadLocker l(&m_channelsLock);
74✔
153
    return m_channels;
148✔
154
}
74✔
155

156
int GenericFader::channelsCount() const
×
157
{
158
    QReadLocker l(&m_channelsLock);
×
159
    return m_channels.count();
×
160
}
×
161

162
FadeChannel &GenericFader::channelFaderLocked(const Doc *doc, Universe *universe, quint32 fixtureID, quint32 channel)
855,212✔
163
{
164
    const quint32 requestedHash = channelHash(fixtureID, channel);
855,212✔
165

166
    QSharedPointer<FadeChannel> channelTemplate = m_channelCache.value(requestedHash);
855,212✔
167
    if (channelTemplate.isNull())
855,212✔
168
    {
169
        channelTemplate = QSharedPointer<FadeChannel>(new FadeChannel(doc, fixtureID, channel));
60,587✔
170
        m_channelCache.insert(requestedHash, channelTemplate);
60,587✔
171
    }
172

173
    const quint32 primary = channelTemplate->primaryChannel();
855,212✔
174
    const quint32 hash = (handleSecondary() && primary != QLCChannel::invalid())
885,727✔
175
                         ? channelHash(channelTemplate->fixture(), primary)
855,212✔
176
                         : channelHash(channelTemplate->fixture(), channelTemplate->channel());
855,212✔
177

178
    QHash<quint32, FadeChannel>::iterator channelIterator = m_channels.find(hash);
855,212✔
179
    if (channelIterator != m_channels.end())
855,212✔
180
    {
181
        FadeChannel &fc = channelIterator.value();
794,626✔
182
        if (handleSecondary() &&
794,626✔
NEW
183
            fc.channelCount() == 1 &&
×
184
            primary != QLCChannel::invalid() &&
794,626✔
185
            channel != primary)
186
        {
NEW
187
            fc.addChannel(channel);
×
NEW
188
            if (universe)
×
NEW
189
                fc.setCurrent(universe->preGMValue(fc.address() + 1), 1);
×
190
        }
191
        return fc;
794,626✔
192
    }
193

194
    FadeChannel fc;
60,586✔
195
    if (handleSecondary() && primary != QLCChannel::invalid() && channel != primary)
60,586✔
196
    {
NEW
197
        const quint32 primaryRequestHash = channelHash(fixtureID, primary);
×
NEW
198
        QSharedPointer<FadeChannel> primaryTemplate = m_channelCache.value(primaryRequestHash);
×
NEW
199
        if (primaryTemplate.isNull())
×
200
        {
NEW
201
            primaryTemplate = QSharedPointer<FadeChannel>(new FadeChannel(doc, fixtureID, primary));
×
NEW
202
            m_channelCache.insert(primaryRequestHash, primaryTemplate);
×
203
        }
204

NEW
205
        fc = *primaryTemplate;
×
NEW
206
        fc.addChannel(channel);
×
207

NEW
208
        if (universe)
×
209
        {
NEW
210
            fc.setCurrent(universe->preGMValue(fc.address()), 0);
×
NEW
211
            fc.setCurrent(universe->preGMValue(fc.address() + 1), 1);
×
212
        }
NEW
213
    }
×
214
    else
215
    {
216
        fc = *channelTemplate;
60,586✔
217
        if (universe)
60,586✔
218
            fc.setCurrent(universe->preGMValue(fc.address()));
60,586✔
219
    }
220

221
    channelIterator = m_channels.insert(hash, fc);
60,586✔
222
    return channelIterator.value();
60,586✔
223
}
855,212✔
224

225
void GenericFader::write(Universe *universe, uint elapsedMs)
1,323,198✔
226
{
227
    if (m_monitoring)
1,323,198✔
228
        emit preWriteData(universe->id(), universe->preGMValues());
×
229

230
    qreal compIntensity = intensity() * parentIntensity();
1,323,198✔
231

232
    //qDebug() << "[GenericFader] writing channels: " << this << m_channels.count();
233

234
    // iterate through all the channels handled by this fader
235
    QWriteLocker l(&m_channelsLock);
1,323,198✔
236
    QMutableHashIterator <quint32,FadeChannel> it(m_channels);
1,323,198✔
237
    while (it.hasNext() == true)
12,943,451✔
238
    {
239
        FadeChannel& fc(it.next().value());
11,620,253✔
240
        int flags = fc.flags();
11,620,253✔
241
        quint32 address = fc.addressInUniverse();
11,620,253✔
242
        int channelCount = fc.channelCount();
11,620,253✔
243

244
        if (address == QLCChannel::invalid())
11,620,253✔
245
        {
246
            qWarning() << "Invalid channel found";
×
247
            continue;
940,508✔
248
        }
249

250
        if (flags & FadeChannel::SetTarget)
11,620,253✔
251
        {
252
            fc.removeFlag(FadeChannel::SetTarget);
2✔
253
            fc.addFlag(FadeChannel::AutoRemove);
2✔
254
            for (int i = 0; i < channelCount; i++)
4✔
255
                fc.setTarget(universe->preGMValue(address + i), i);
2✔
256
        }
257

258
        // Calculate the next step
259
        if (m_paused == false)
11,620,253✔
260
            fc.nextStep(elapsedMs);
11,620,253✔
261

262
        quint32 value = fc.current();
11,620,253✔
263

264
        // Apply intensity to channels that can fade
265
        if (fc.canFade())
11,620,253✔
266
        {
267
            if ((flags & FadeChannel::CrossFade) && fc.fadeTime() == 0)
11,620,253✔
268
            {
269
                // morph start <-> target depending on intensities
270
                bool rampUp = fc.target() > fc.start() ? true : false;
20✔
271
                value = rampUp ? fc.target() - fc.start() : fc.start() - fc.target();
20✔
272
                value = qreal(value) * intensity();
20✔
273
                value = qreal(rampUp ? fc.start() + value : fc.start() - value) * parentIntensity();
20✔
274
            }
275
            else if (flags & FadeChannel::Intensity)
11,620,233✔
276
            {
277
                value = fc.current(compIntensity);
6,581,478✔
278
            }
279
        }
280

281
        //qDebug() << "[GenericFader] >>> uni:" << universe->id() << ", address:" << address << ", value:" << value << "int:" << compIntensity;
282
        if (flags & FadeChannel::Override)
11,620,253✔
283
        {
284
            universe->write(address, value, true);
470,250✔
285
            continue;
470,250✔
286
        }
287
        else if (flags & FadeChannel::Relative)
11,150,003✔
288
        {
289
            universe->writeRelative(address, value, channelCount);
495,000✔
290
        }
291
        else if (flags & FadeChannel::Flashing)
10,655,003✔
292
        {
293
            for (int i = 0; i < channelCount; i++)
940,516✔
294
                universe->write(address + i, ((uchar *)&value)[channelCount - 1 - i],
470,258✔
295
                                flags & FadeChannel::ForceLTP ? true : false);
470,258✔
296
            continue;
470,258✔
297
        }
470,258✔
298
        else
299
        {
300
            // treat value as a whole, so do this just once per FadeChannel
301
            universe->writeBlended(address, value, channelCount, m_blendMode);
10,184,745✔
302
        }
303

304
        if (((flags & FadeChannel::Intensity) &&
10,679,745✔
305
            (flags & FadeChannel::HTP) &&
5,640,970✔
306
            m_blendMode == Universe::NormalBlend) || m_fadeOut)
10,679,745✔
307
        {
308
            // Remove all channels that reach their target _zero_ value.
309
            // They have no effect either way so removing them saves a bit of CPU.
310
            if (fc.current() == 0 && fc.target() == 0 && fc.isReady())
1,408,724✔
311
                it.remove();
37✔
312
        }
313

314
        if (flags & FadeChannel::AutoRemove && value == fc.target())
10,679,745✔
315
            it.remove();
2✔
316
    }
317

318
    // self-request deletion when fadeout is complete
319
    if (m_fadeOut && m_channels.isEmpty())
1,323,198✔
320
    {
321
        m_fadeOut = false;
2✔
322
        requestDelete();
2✔
323
    }
324
}
1,323,198✔
325

326
qreal GenericFader::intensity() const
1,323,219✔
327
{
328
    return m_intensity;
1,323,219✔
329
}
330

331
void GenericFader::adjustIntensity(qreal fraction)
3,280✔
332
{
333
    //qDebug() << name() << "I FADER intensity" << fraction << ", PARENT:" << m_parentIntensity;
334
    m_intensity = fraction;
3,280✔
335
}
3,280✔
336

337
qreal GenericFader::parentIntensity() const
1,323,218✔
338
{
339
    return m_parentIntensity;
1,323,218✔
340
}
341

342
void GenericFader::setParentIntensity(qreal fraction)
125✔
343
{
344
    //qDebug() << name() << "P FADER intensity" << m_intensity << ", PARENT:" << fraction;
345
    m_parentIntensity = fraction;
125✔
346
}
125✔
347

348
bool GenericFader::isPaused() const
×
349
{
350
    return m_paused;
×
351
}
352

353
void GenericFader::setPaused(bool paused)
5✔
354
{
355
    m_paused = paused;
5✔
356
}
5✔
357

358
bool GenericFader::isEnabled() const
1,323,097✔
359
{
360
    return m_enabled;
1,323,097✔
361
}
362

363
void GenericFader::setEnabled(bool enable)
×
364
{
365
    m_enabled = enable;
×
366
}
×
367

368
bool GenericFader::isFadingOut() const
4✔
369
{
370
    return m_fadeOut;
4✔
371
}
372

373
void GenericFader::setFadeOut(bool enable, uint fadeTime)
6✔
374
{
375
    m_fadeOut = enable;
6✔
376

377
    if (fadeTime == 0)
6✔
378
        return;
×
379

380
    QWriteLocker l(&m_channelsLock);
6✔
381
    QMutableHashIterator <quint32,FadeChannel> it(m_channels);
6✔
382
    while (it.hasNext() == true)
24✔
383
    {
384
        FadeChannel& fc(it.next().value());
18✔
385

386
        fc.setStart(fc.current());
18✔
387
        // if not HTP and/or flashing, request channels
388
        // to target the current universe value
389
        // (will be handled in the write method)
390
        if (((fc.flags() & FadeChannel::Flashing) == 0) &&
36✔
391
            ((fc.flags() & FadeChannel::Intensity) == 0))
18✔
392
            fc.addFlag(FadeChannel::SetTarget);
6✔
393
        fc.setTarget(0);
18✔
394
        fc.setElapsed(0);
18✔
395
        fc.setReady(false);
18✔
396
        fc.setFadeTime(fc.canFade() ? fadeTime : 0);
18✔
397
        // if flashing, remove the flag and treat
398
        // it like a regular fade out to target
399
        fc.removeFlag(FadeChannel::Flashing);
18✔
400
    }
401
}
6✔
402

403
void GenericFader::setBlendMode(Universe::BlendMode mode)
2,523✔
404
{
405
    m_blendMode = mode;
2,523✔
406
}
2,523✔
407

408
void GenericFader::setMonitoring(bool enable)
×
409
{
410
    m_monitoring = enable;
×
411
}
×
412

413
void GenericFader::resetCrossfade()
×
414
{
415
    qDebug() << name() << "resetting crossfade channels";
×
NEW
416
    QWriteLocker l(&m_channelsLock);
×
417
    QMutableHashIterator <quint32,FadeChannel> it(m_channels);
×
418
    while (it.hasNext() == true)
×
419
    {
420
        FadeChannel& fc(it.next().value());
×
421
        fc.removeFlag(FadeChannel::CrossFade);
×
422
    }
423
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc