Skip to content

fix(DlgPrefController): add missing layout #14606

Merged
daschuer merged 1 commit intomixxxdj:2.6from
acolombier:fix/dlg-controller-assert
Oct 28, 2025
Merged

fix(DlgPrefController): add missing layout #14606
daschuer merged 1 commit intomixxxdj:2.6from
acolombier:fix/dlg-controller-assert

Conversation

@acolombier
Copy link
Copy Markdown
Member

Fix asserts introduced in #14006 and deadlock introduced in #14159

Comment thread src/controllers/dlgprefcontroller.cpp Outdated
Comment on lines +239 to +253
&ControllerManager::slotApplyMapping);

// Update GUI
connect(m_pControllerManager.get(),
&ControllerManager::mappingApplied,
this,
&DlgPrefController::enableWizardAndIOTabs);

connect(m_pControllerManager.get(),
&ControllerManager::mappingApplied,
this,
[this](bool) {
// shortcut for creating and assigning required I/O table models
showMapping(m_pMapping);
});
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this fix is incomplete, but I'm not sure what the Qt::BlockingQueuedConnection was intending to fix. @daschuer do you remember?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I know. Probably the comment was not clear enough. We have two copies of the mapping, one is the shared copy in the preferences, the user can edit. The other is the immutable copy that is used in the controllers thread.
We must prevent that the mapping is edited in the main thread while copying it. Else we have a corrupted copy that may lead to a crash.

The blocking connection stops the main thread that the mapping cannot he edited.

I think a solution her is to introduce two calls. One copying the mapping and a second one that interacts with the GUI thread after we have the immutable copy.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think my fix is sufficient then? IIUC, this change will load and bind the GUI component once the controller has acknowledge the newly copied mapping. Or do it it further changes?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the blocking connection this:

m_pMapping = downcastAndClone<LegacyMidiControllerMapping>(pMapping.get());

Is called from the controller thread while the GUI thread has still write access to the mapping.

A workaround would be to restore the blocking call and do the controller GUI initialization in a second slot without a lock.

The copying is done in the controller thread, because the controller thread is this way also like locked implicit.

An alternative would be to clone the mapping twice.
First create a copy in the GUI thread. Than we have a temporary immutable mapping. Than copy it again in the controller thread by swapping pointers.

I like to avoid locking in the controller thread, because the mapping loockups happen on a high rate.

Copy link
Copy Markdown
Member Author

@acolombier acolombier Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A workaround would be to restore the blocking call and do the controller GUI initialization in a second slot without a lock.

This is not possible. Here is sequence diagram of the event loop

sequenceDiagram
    GUI->>Controller: applyMapping
    activate Controller
    critical setup resources in main thread
    Controller-->>GUI: setup resources in main thread
    end
Loading

This means that there is no way we can keep applyMapping a blocking call and keep this working. Please bare in mind that the lock in controller is out of our remit (see doc)

I like to avoid locking in the controller thread, because the mapping loockups happen on a high rate.
I'm not sure to understand what this has to do here? The deadlock doesn't happen at loockup but at applyMapping (a.k.a controller initailisation)

Basically, we use applyMapping in 3 occurrences in the preference screens:

  • MIDI mapping (which seems to justify you adding that blocking connection)
  • Mapping selection (which will deadlock if loading a screen mapping
  • Default mapping selection (which will deadlock if loading a screen mapping

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lockinh needs to be installed whenever the mapping is accessed, because of of these calls can be happen and it causes a crash.

A workaround would be to restore the blocking call and do the controller GUI initialization in a second slot without a lock.

This is not possible. Here is sequence diagram of the event loop

For my understanding the workflow can remain. We just need to take care that the mapping is not altered during setting up the resources in main. That's why I think that one blocking call for the mutable stuff and one other call for the rest will work.

Whenever the user changes a preferences option that affecte the mapping, we need to repeat the trick. Editing the mapping in the GUI thread an apply in the blocking call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a mutex to slotApplyMapping then? Since the deadlock comes from blocking the whole thread's event loop, restricting the critical section should fix this issue

@acolombier acolombier changed the title fix(DlgPrefController): remove invalid assert Fix various crashes in controller preferences Apr 7, 2025
@acolombier acolombier added this to the 2.6.0 milestone Apr 7, 2025
@JoergAtGithub JoergAtGithub mentioned this pull request Apr 7, 2025
@JoergAtGithub
Copy link
Copy Markdown
Member

Could you please retarget the fix for the deadlock introduced by #14159 to 2.5.

@acolombier acolombier force-pushed the fix/dlg-controller-assert branch from a234657 to 8e6f75c Compare April 7, 2025 20:51
@acolombier acolombier changed the base branch from main to 2.5 April 7, 2025 20:51
@acolombier
Copy link
Copy Markdown
Member Author

Done - #14006 to be addressed separately

@JoergAtGithub JoergAtGithub modified the milestones: 2.6.0, 2.5.1 Apr 7, 2025
@JoergAtGithub JoergAtGithub requested a review from daschuer April 7, 2025 20:52
@acolombier acolombier changed the base branch from 2.5 to main April 7, 2025 20:53
@acolombier acolombier force-pushed the fix/dlg-controller-assert branch from 8e6f75c to a234657 Compare April 7, 2025 20:53
@acolombier
Copy link
Copy Markdown
Member Author

Actually, I reverted the PR for 2.6. The deadlock only impact that branch as this is related to QML screen controller (see comment here), so we could release 2.5.1 without this change. WDYT?

@JoergAtGithub
Copy link
Copy Markdown
Member

Ok, makes sense!

@JoergAtGithub JoergAtGithub modified the milestones: 2.5.1, 2.6.0 Apr 7, 2025
@acolombier acolombier changed the base branch from main to 2.6 April 29, 2025 21:23
@acolombier acolombier mentioned this pull request May 11, 2025
3 tasks
@acolombier acolombier force-pushed the fix/dlg-controller-assert branch from a234657 to 57d4abc Compare May 11, 2025 12:43
@github-actions github-actions Bot added library ui build code quality waveform developer experience Issues, bugs and PRs related to the development process, development environment & developer docs labels May 11, 2025
@JoergAtGithub
Copy link
Copy Markdown
Member

Please resolve the merge conflicts!

@acolombier acolombier force-pushed the fix/dlg-controller-assert branch from 57d4abc to 28761d7 Compare May 12, 2025 18:45
@acolombier acolombier added preferences and removed build code quality waveform controller backend developer experience Issues, bugs and PRs related to the development process, development environment & developer docs labels Oct 17, 2025
@acolombier
Copy link
Copy Markdown
Member Author

Can you show a backtrace form the dead lock situation?

You can see a backtrace here

@acolombier
Copy link
Copy Markdown
Member Author

acolombier commented Oct 17, 2025

Okay, so let's sum up where we have:

  • We must prevent concurrent access to the controller mapping, which occur between the main thread and the controller thread
  • We cannot block the main thread as it will need its eventloop to be running to allow offscreen rendering resources to be created when ControllerManager::openController is called.

This means we cannot using Qt::BlockingQueuedConnection, which will block the thread (and thus the eventloop) till the controller slot has completed.

Here are the solutions I can thing about:

  1. Ensure concurrent access at the LegacyControllerMapping level by introducing a lock, similar to how Track works
  2. Block the main thread with explicit means (not Qt::BlockingQueuedConnection) and introduce a new signal to be emitted when ControllerManager::openController starts, which would lift the lock on main
  3. Duplicate the LegacyControllerMapping and remove the shared_ptr, leading to concurrent access across threads

1) analysis:

  • Pro: we ensure no change on the shared access design that seems to be used in various place
  • Neutral: no performance impact because mapping are not accessed/edited at runtime and is only a one-off when starting a controller.
  • Con: we add a lot of boilerplate, similar to what there is in Track

2) analysis:

  • Pro: we ensure no change on the shared access design that seems to be used in various place
  • Neutral: no performance impact because mapping are not accessed/edited at runtime and is only a one-off when starting a controller.
  • Con: we add another signal a strong dependency on the main thread locking state.

3) analysis:

  • Pro: Kill the concurrent access risk at the root, without locking overhead
  • Con: technically breaking change to the API, with a risk of losing the currently assumed shared instances. Hard to confirm it won't add any regression

From this, it feels like the "least worse" option in order would be:

  • 2)
  • 1)
  • ... and 3) (major change on a release candidate!)

wdyt?

@daschuer
Copy link
Copy Markdown
Member

daschuer commented Oct 17, 2025

We already have 3. for editing the mappings itself. So a natural solution would be to extend this for the controller preferences as well.

Con: technically breaking change to the API, with a risk of losing the currently assumed shared instances. Hard to confirm it won't add any regression

Can you explain that a bit more? Which API will be changed?

I think we need to separate two things. Applying the preferences to the JS Code and on the fly changes of values. Do we have the later? For my understanding we always apply all settings at once.

@acolombier
Copy link
Copy Markdown
Member Author

We already have 3. for editing the mappings itself. So a natural solution would be to extend this for the controller preferences as well.

Can you clarify this? I understand that there is some space where we clone the mapping, but these are localised usecase. Are we sure this won't have any impact otherwise?

Can you explain that a bit more? Which API will be changed?

Sure, my fear is that current we rely on a shared_ptr<Mapping> instance, which implies that the data in shared between the Controller and main. I don't have a full picture on why and how this is currently - for example, no idea if this is potentially used during the point-n-click of MIDI learning, where the controller and main rely on a single mapping instance?
This shared ptr is currently used in many cases (all controller types impl) and beside my moderate experience on this part of the code, I cannot say for sure if removing this shared_ptr would break sync of the data across thread.

If you are 100% sure it won't, I'm happy to go down this road.

I think we need to separate two things. Applying the preferences to the JS Code and on the fly changes of values. Do we have the later? For my understanding we always apply all settings at once

Oh yeah, for now it's the latter, tho there was discussions to bring the former as an option in the future (and a split would likely cripple this feature opportunity)

@daschuer
Copy link
Copy Markdown
Member

Can you clarify this?

We had an issue with writing a used copy of the mapping which was fixed here:
#14159

I have tried to explain how it works in the commit description. Is this understandable of do you have questions?
I think we can treat the mapping options in the same way like the mapping itself and have also a immutable copy when running.

Sure, my fear is that current we rely on a shared_ptr instance, which implies that the data in shared between the Controller and main.

This is used for ownership tracking only. I think this is the significant part:

void BulkController::setMapping(std::shared_ptr<LegacyControllerMapping> pMapping) {
m_pMutableMapping = pMapping;
m_pMapping = downcastAndClone<LegacyHidControllerMapping>(pMapping.get());
}

The controller receives from the settings a shared pointer which tracks that it is not deleted when one thread outlives the other. In addition we do a deep copy into std::unique_ptr<LegacyHidControllerMapping> m_pMapping which is than used excursively from the controller thread.

If you are 100% sure it won't, I'm happy to go down this road.

I don't have a crystal ball to be that. :-/

Oh yeah, for now it's the latter, tho there was discussions to bring the former as an option in the future (and a split would likely cripple this feature opportunity)

Like in the mapping itself the values are still mutable only the control structures are not. So runtime changes can be still applied via atomic variables inside the otherwise immutable version of the controller preferences. Like control objects. (or reuse them). The crasher here does is not cased by access to preferences values it is cased because the whole preferences structure has changed behind the back.

@acolombier
Copy link
Copy Markdown
Member Author

I tried to implement the cloning for the last 2h, but couldn't get anything to work that wouldn't be a major refactor. Would you want to push what you had in mind maybe?

@daschuer
Copy link
Copy Markdown
Member

Can you point me to you experimental branch which not works? Why does cloning not work?

@acolombier
Copy link
Copy Markdown
Member Author

Unfortunately, I didn't commit everything and discarded all the changes (twice actually, after hitting round blocks)
The issue was related to the sheer size required by updating the current widely used shared_ptr to unique_ptr/const ptr*. I didn't get to a state I could test something but I was getting 300+ line changes.

I guess I could keep the shared_ptr for now and just do the clone in each Controller::setMapping (which has downcastAndClone), tho there would still be some potentially concurrent access in the ControllerManager slot.

@ywwg
Copy link
Copy Markdown
Member

ywwg commented Oct 20, 2025

I am following this PR with interest would like to understand these issues better and try to help work out a solution. Would you two be open to doing a call to look at the code in depth? Concurrency is one of the things Mixxx struggles with and we want to ensure we don't create technical debt or bad bugs.

@acolombier
Copy link
Copy Markdown
Member Author

Good shout, I'm up for it. Will create a thread on Zulip!

@acolombier
Copy link
Copy Markdown
Member Author

Superseded by #15524

@acolombier acolombier closed this Oct 28, 2025
@acolombier acolombier reopened this Oct 28, 2025
@acolombier
Copy link
Copy Markdown
Member Author

Actually, there is 2ed412f that still need merging!

Copy link
Copy Markdown
Member

@daschuer daschuer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@daschuer daschuer changed the title Fix various crashes in controller preferences fix(DlgPrefController): add missing layout Oct 28, 2025
@daschuer daschuer merged commit 94287d2 into mixxxdj:2.6 Oct 28, 2025
25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants