From 631255c3cbab448f73b860f9d74698b4257a0691 Mon Sep 17 00:00:00 2001 From: Peter Feerick <5500713+pfeerick@users.noreply.github.com> Date: Wed, 4 Nov 2026 02:03:41 +0000 Subject: [PATCH] fix(cpn): avoid deadlock when closing the simulator during boot WasmSimulatorInterface::stop() holds m_mutex across simuStop(), which joins the firmware threads. Those threads read analog inputs through host_simuGetAnalog -> getAnalogValue(), which also took m_mutex. If a firmware thread read an analog after stop() took the lock, the join never returned. The GUI thread then blocked on the same mutex in isRunning(), so the 2s stop timeout never fired and Companion hung. This shows up when the simulator is closed while the firmware is still booting, because edgeTxInit() reads analogs (throttle/switch checks, splash inactivity check) before the menus task can exit. Store the analog values as atomics so getAnalogValue() and setAnalogValue() no longer need m_mutex. Also close the SDL audio device after simuStop() rather than before, so the firmware audio task cannot be inside SDL_QueueAudio() while the device is closed. Co-Authored-By: Claude Opus 5.5 --- .../src/simulation/wasmsimulatorinterface.cpp | 16 ++++++++++------ .../src/simulation/wasmsimulatorinterface.h | 4 +++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/companion/src/simulation/wasmsimulatorinterface.cpp b/companion/src/simulation/wasmsimulatorinterface.cpp index d9081741e50..44746a2cfe5 100644 --- a/companion/src/simulation/wasmsimulatorinterface.cpp +++ b/companion/src/simulation/wasmsimulatorinterface.cpp @@ -444,9 +444,11 @@ void WasmSimulatorInterface::init() m_stopRequested = false; + for (auto & value : m_analogValues) + value.store(0, std::memory_order_relaxed); + // Call simuInit QMutexLocker lckr(&m_mutex); - memset(m_analogValues, 0, sizeof(m_analogValues)); wasm_runtime_call_wasm(m_execEnv, m_fnInit, 0, nullptr); // Query LCD dimensions @@ -584,13 +586,16 @@ void WasmSimulatorInterface::stop() return; m_stopRequested = true; - deinitAudio(); { QMutexLocker lckr(&m_mutex); wasm_runtime_call_wasm(m_execEnv, m_fnStop, 0, nullptr); } + // Close audio only once simuStop() has joined the firmware threads, so the + // audio task cannot be inside SDL_QueueAudio() while the device closes. + deinitAudio(); + // Fully destroy the WASM module so the next init()/start() gets a clean // instance. WASI threads created by the firmware's RTOS layer cannot be // reliably restarted within the same module instance. @@ -614,16 +619,15 @@ void WasmSimulatorInterface::setVolumeGain(const int value) void WasmSimulatorInterface::setAnalogValue(uint8_t index, int16_t value) { - QMutexLocker lckr(&m_mutex); if (index < MAX_ANALOGS) - m_analogValues[index] = value; + m_analogValues[index].store(value, std::memory_order_relaxed); } +// Called from firmware threads; must not take m_mutex (see header). int16_t WasmSimulatorInterface::getAnalogValue(uint8_t index) { - QMutexLocker lckr(&m_mutex); if (index < MAX_ANALOGS) - return m_analogValues[index]; + return m_analogValues[index].load(std::memory_order_relaxed); return 0; } diff --git a/companion/src/simulation/wasmsimulatorinterface.h b/companion/src/simulation/wasmsimulatorinterface.h index 6daebe8918f..1d8596f6606 100644 --- a/companion/src/simulation/wasmsimulatorinterface.h +++ b/companion/src/simulation/wasmsimulatorinterface.h @@ -126,7 +126,9 @@ class WasmSimulatorInterface : public SimulatorInterface // Host-side analog values (polled by WASM via simuGetAnalog) static constexpr int MAX_ANALOGS = 32; - int16_t m_analogValues[MAX_ANALOGS] = {}; + // Read by firmware threads through host_simuGetAnalog, so kept out of + // m_mutex: stop() holds that across simuStop(), which joins those threads. + std::atomic m_analogValues[MAX_ANALOGS] = {}; // Cached output values for change detection TxOutputs m_lastOutputs;