Engine and Playback Fixes - mjcmatrix/qlcplus GitHub Wiki
Under-the-hood playback correctness fixes, plus app-wide UI fixes that don't belong to any single editor. Mostly invisible until they bite, so this page is bullet-only.
Nothing in this section has been merged upstream yet.
-
DMX output no longer waits for a busy UI. (Fork #111) Each engine tick computed its values on time, but the wake-up that sends them to the outputs went through the UI thread's event queue. Whenever the UI was busy (a heavy 3D view, a large repaint, loading), output was delayed: with a busy 3D view, a 500 ms tempo-section step landed up to 64–95 ms early or late, about ±25 ms typically. So the lagging Tempo indicator under a busy 3D view wasn't only cosmetic. The engine timer now wakes the outputs itself. The same test now stays within 7 ms with frame pacing off and 3 ms with it on (0.9 ms with the 3D view closed). Input, passthrough and external beat sources still go through the UI thread.
➖ Upstream: not yet sent individually
-
Beats-tempo EFX now run on the beats. (Fork #89, #91) An EFX could be set to Beats tempo, but the engine still read its loop duration and fade in (stored in 1/1000 beats) as milliseconds. They now run on the global BPM, locking onto the beat without jumping and free-running at the BPM if the beat generator is off. The EFX editor gains a Tempo type selector. Changing the tempo type in any Function editor (RGB Matrix, Scene, EFX) no longer converts the speeds twice (2 s at 120 BPM became 8 beats rather than 4), and the editors' tempo selectors follow changes made elsewhere such as undo/redo. A Chaser tempo type change can now be undone.
➖ Upstream: not yet sent individually
-
A running Show follows edits made to it. (Fork #90) Items added, moved, resized or deleted, Functions deleted, tracks added or muted and tempo sections edited while a Show plays (or is paused) now take effect straight away. The Show runner used to keep raw pointers to deleted items and Functions, which caused use-after-free crashes; it now tracks them by ID. Muting a track still doesn't stop items already playing.
➖ Upstream: not yet sent individually
-
Paused faders now complete their fade-out. (Fork #69) A fader paused mid-Scene (e.g. by pausing a Show) that was then told to fade out — by stopping the paused Show, or restarting it from a new cursor position — never advanced, because pausing and fading-out were independent flags and nothing cleared the pause. The stuck fader held its last value on the DMX output indefinitely, until the whole project was reloaded. Fixed by having fade-out clear the pause flag, matching how Chasers already handle this for their own steps.
🔧 Related fix: a paused Chaser also pauses the faders of its previous step's Scene, which may still be fading out, and those could be left paused for good. No Function owned them any more, so neither stopping the Show nor "Stop all functions" cleared them, and a dim colour stayed on the output until QLC+ was restarted. The Chaser now records exactly which faders it paused and always releases them when it stops, even if their Scene was deleted during the pause, and it no longer advances a step on the tick it pauses. Builds on the fix above. (Fork #105)
➖ Upstream: not yet sent individually
-
VC Adjust slider no longer stops a just-restarted Function. (Fork #70) An Adjust-mode slider reflecting a Function's stop (e.g. from the fader fix above, or any stop/restart in quick succession) could queue a 0-adjustment that landed after the Function had already been restarted, stopping it again. The slider now ignores a stale stop notification if the Function has since restarted, and shows a stop without queuing a 0 write when nothing needs to receive it.
➖ Upstream: not yet sent individually
-
Show audio start held until actually heard. (Fork #72) A Show's timeline used to start counting the instant an Audio item was told to start, while a suspended audio device can take hundreds of milliseconds to actually begin playing — so every light cue ran early against the music for the rest of that playback. The Show now holds its clock (up to ~300–400 ms, with a 2 s safety timeout) until the audio device reports it has actually started playing, on both a cold start and a resume from pause. Backends that can't report playback position (ALSA, PortAudio, CoreAudio, WaveOut) are unaffected and behave as before.
🔧 Follow-up fix: items ending exactly where an Audio item begins were frozen along with everything else for the whole hold, and only stopped once it was released, instead of on time. A Chaser item stopped that way was one of the ways faders were left stuck (see the related fix above). Ended items are now stopped before the hold starts. As a side effect, an Audio item ending exactly where another item of the same Audio begins is now stopped and restarted, as any other Function already is. (Fork #106)
➖ Upstream: not yet sent individually
-
Chaser steps started part way in no longer run a tick ahead. (Fork #108) A Show starts a Chaser part way into its first step whenever the item begins between two engine ticks, which is very common for items placed in ms. That step was counted one tick ahead, so with an item as long as its steps the Chaser started its next step for a single tick just before the Show stopped it, a visible flash at the end of the item. The step is now counted from the next tick like any other. Steps set directly, such as a Cue List jump, likewise now last their full duration instead of one tick less.
➖ Upstream: not yet sent individually; a branch based on
upstream/masteris ready -
Channel modifiers no longer lost on load/save. (Fork #9) A
<Modifier>element whose template wasn't found in the modifiers cache used to be silently discarded on load, andsaveXML()then permanently deleted it from the project file on the next save — with a real runtime consequence: a Forced-HTP shutter channel relying on anAlways Fullmodifier would go dark once its modifier was gone. The modifier name is now stored and round-tripped even when the template can't be resolved.🕓 Upstream: open, not yet merged (mcallegari/qlcplus#2121)
-
No more thread warnings when audio waveforms are drawn. (Fork #101) Opening a project with Audio items in a Show, or dragging the window onto another monitor, printed a
QObject: Cannot create children for a parent that is in a different threadwarning for every waveform. The audio plugin cache gave its temporary plugin loaders itself as parent, but it lives in the main thread while the Show Manager waveforms and BPM auto-detection open audio files from background threads. The parent did nothing, so it's gone; behaviour is otherwise unchanged. The waveform part also affects upstream.➖ Upstream: not yet sent individually
These aren't tied to one editor — they affect the whole application.
-
Actions menu sub-menus fixed on Qt 6.11, and scrollbars added to the file dialogs. (Fork #5) On Qt 6.11 the Actions menu's sub-menus opened in the wrong place (or not at all); the file Open/Save dialogs also had no scrollbar, so a folder with more entries than fitted couldn't be browsed with the mouse alone.
✅ Upstream: merged (mcallegari/qlcplus#2118)
-
Fixed a crash clicking away from the Actions menu with a sub-menu open. (Fork #31)
✅ Upstream: merged (mcallegari/qlcplus#2157)
-
Fixed a crash opening a Function editor while another of the same type was open. (Fork #79) Double-clicking one Collection item on the Show Manager timeline and then the next one segfaulted every time — likewise for Scenes, Chasers/Sequences and EFXs. The editor's constructor published itself to QML before its list model existed, which immediately re-evaluated the still-loaded previous editor's bindings against a half-built object. The editors now publish themselves only once fully constructed.
✅ Upstream: merged (mcallegari/qlcplus#2169)
-
The Actions menu now closes when it asks to save the project first. (Fork #32) The menu stayed open behind the "save first?" dialog, so it was still sitting there over the app after the dialog was answered.
➖ Upstream: not yet sent individually
-
Mouse wheel no longer changes combo box values. (Fork #75) Every drop-down in QLC+ had wheel scrolling enabled, so scrolling with the pointer happening to sit over one silently changed its value instead of scrolling the list underneath. Worst in the Fixtures editor's expanded channel list, where the Modifier and Precedence columns sit exactly where the pointer rests while scrolling through channels — a stray scroll could quietly rewrite a channel's modifier. The wheel now only scrolls the containing list. One change to the shared
CustomComboBoxcovers every drop-down in the app.➖ Upstream: formal PR not submitted; PR was sent as mcallegari/qlcplus#2164 prematurely in error and closed without merging by the submitter
-
Editable combo boxes now actually work. (Fork #98) A drop-down whose text can also be typed into — the Fixture Editor's bulb type, and now the Show Manager's Show selector — accepted neither. Typing never reached the control, because Qt only wires the text up when the combo box's content item is the text input, and in the shared
CustomComboBoxit sits nested inside a container; and the text field spanned the full width of the control, swallowing the click that should have opened the drop-down. Both are fixed inCustomComboBox, so every editable drop-down in the app benefits. Combo box text is also vertically centred now, which removes some slight clipping of descenders on the ordinary read-only drop-downs.➖ Upstream: not yet sent individually
-
Scrollbar no longer jumps in the fixture and function trees. (Fork #95) Dragging the scrollbar in any list longer than a page — the Fixture Manager and its selection panels, the Functions tree, both Import Project trees, the manual input source list, the Stage Wizard fixture lists — made the handle jump away from the pointer on nearly every mouse move, and the bottom of the list couldn't be reached. Those lists hold items of wildly different heights (an expanded tree node is as tall as all its children), and a
ListViewonly measures the delegates it currently has loaded, estimating the rest: as items came and went during the drag, the estimated content height moved and took the handle with it. The affected views now keep all their top-level delegates loaded, so the scrollbar is sized against the real content height. Costs nothing extra — the tree nodes build their children with aRepeaterregardless.➖ Upstream: not yet sent individually