@@ -939,26 +939,33 @@ MainComponent::MainComponent()
939939 startTimerHz (60 );
940940 startAudioDeviceScan ();
941941
942- // GPU-accelerated rendering (Windows only).
943- // On Windows, JUCE's OpenGL context offloads image compositing from GDI
944- // to the GPU, reducing message-thread load from repaint().
945- // On macOS, this is DISABLED: JUCE still renders into software images
946- // and then uploads them as textures through Apple's deprecated
947- // OpenGL-to-Metal translation layer, which adds overhead rather than
948- // reducing it. CoreGraphics already uses Metal internally for
949- // compositing, so native rendering is faster without OpenGL.
950- #if JUCE_WINDOWS
951- glContext.attachTo (*this );
952- #endif
942+ // GPU-accelerated rendering: DISABLED for thread safety.
943+ //
944+ // When glContext.attachTo(*this) is active, JUCE calls paint() for ALL
945+ // child components on the OpenGL thread -- not the message thread.
946+ // Meanwhile, timerCallback() writes juce::String members (artist, title,
947+ // playState, sourceName, etc.) on the message thread. juce::String is
948+ // reference-counted: a concurrent read during write can corrupt the
949+ // refcount and crash. This affects TimecodeDisplay, ProDJLinkView,
950+ // all juce::Label instances, and any component that reads String data
951+ // in paint().
952+ //
953+ // JUCE's OpenGL renderer paints into software images on the CPU and
954+ // only uses the GPU for the final texture upload + composite. With
955+ // the waveform image cache, HiDPI deck image cache, and targeted
956+ // dirty-rect repainting already in place, the performance difference
957+ // is negligible. Windows DWM already hardware-accelerates the native
958+ // GDI composite path.
959+ //
960+ // For a live performance application, eliminating the entire class of
961+ // GL-thread data races is worth more than the marginal compositing
962+ // speedup. If GPU acceleration is ever re-enabled, all juce::String
963+ // members read in paint() must be replaced with atomic-safe types
964+ // (fixed char arrays, packed atomics, or a SpinLock-protected snapshot).
953965}
954966
955967MainComponent::~MainComponent ()
956968{
957- // 0. Detach OpenGL before destroying any components (Windows only)
958- #if JUCE_WINDOWS
959- glContext.detach ();
960- #endif
961-
962969 // 1. Stop our UI timer first -- no more timerCallback() after this
963970 stopTimer ();
964971
@@ -1021,9 +1028,15 @@ MainComponent::~MainComponent()
10211028 // 9. Explicitly shut down each engine (timers, threads, sockets)
10221029 // BEFORE engines.clear() destroys the objects, so all HighResolutionTimer
10231030 // threads are stopped while the message manager is still alive.
1031+ // Also disconnect shared pointers (TrackMap, MixerMap, ProDJLink, DbServer)
1032+ // so no stale references survive into AppSettings destruction.
10241033 for (auto & eng : engines)
10251034 {
10261035 eng->setMidiClockEnabled (false );
1036+ eng->setTrackMap (nullptr );
1037+ eng->setMixerMap (nullptr );
1038+ eng->setSharedProDJLinkInput (nullptr );
1039+ eng->setDbServerClient (nullptr );
10271040 eng->stopMtcOutput ();
10281041 eng->stopArtnetOutput ();
10291042 eng->stopLtcOutput ();
@@ -1033,7 +1046,7 @@ MainComponent::~MainComponent()
10331046 eng->stopLtcInput ();
10341047 }
10351048
1036- // 9 . Now safe to destroy engine objects
1049+ // 10 . Now safe to destroy engine objects
10371050 engines.clear ();
10381051}
10391052
@@ -1083,6 +1096,12 @@ void MainComponent::removeEngine(int index)
10831096
10841097 // Explicitly stop all protocols on the engine being deleted BEFORE
10851098 // erasing it, so destructors don't race with any pending callbacks.
1099+ // Disconnect shared pointers first to prevent stale access during stop.
1100+ engines[(size_t )index]->setTrackMap (nullptr );
1101+ engines[(size_t )index]->setMixerMap (nullptr );
1102+ engines[(size_t )index]->setSharedProDJLinkInput (nullptr );
1103+ engines[(size_t )index]->setDbServerClient (nullptr );
1104+ engines[(size_t )index]->setMidiClockEnabled (false );
10861105 engines[(size_t )index]->getTriggerOutput ().setSharedMidiOutput (nullptr );
10871106 engines[(size_t )index]->stopMtcOutput ();
10881107 engines[(size_t )index]->stopArtnetOutput ();
@@ -1732,7 +1751,7 @@ void MainComponent::openMixerMapEditor()
17321751 }
17331752
17341753 auto * editor = new MixerMapEditor (sharedMixerMap,
1735- sharedProDJLinkInput.getMixerChannelCount () > 4 );
1754+ djmModelFromString ( sharedProDJLinkInput.getDJMModel ()) );
17361755 editor->onChange = [this ]
17371756 {
17381757 sharedMixerMap.save ();
@@ -3944,8 +3963,11 @@ void MainComponent::timerCallback()
39443963 // uses its own audio-callback-driven auto-increment, so it's similarly
39453964 // decoupled. If UI stalls (resize, modal dialog), outputs continue
39463965 // transmitting the last-known timecode until the next tick() updates it.
3947- for (auto & eng : engines)
3948- eng->tick ();
3966+ for (int i = 0 ; i < (int )engines.size (); ++i)
3967+ {
3968+ engines[(size_t )i]->setStatusTextVisible (i == selectedEngine);
3969+ engines[(size_t )i]->tick ();
3970+ }
39493971
39503972 // Update UI for selected engine
39513973 auto & eng = currentEngine ();
@@ -4087,7 +4109,10 @@ void MainComponent::timerCallback()
40874109 {
40884110 // Track changed -- clear old waveform immediately (avoids stale cursor)
40894111 waveformDisplay.clearWaveform ();
4090- displayedWaveformTrackId = 0 ;
4112+ // Mark this track as "attempted" so we don't re-enter this block
4113+ // every frame. The retry path below uses hasWaveformData() to
4114+ // detect when the async waveform query completes.
4115+ displayedWaveformTrackId = wfTrackId;
40914116
40924117 // Try to populate from cache (may not have waveform yet)
40934118 juce::String pdlIP = sharedProDJLinkInput.getPlayerIP (pdlPlayer);
@@ -4096,10 +4121,9 @@ void MainComponent::timerCallback()
40964121 {
40974122 waveformDisplay.setColorWaveformData (meta.waveformData ,
40984123 meta.waveformEntryCount , meta.waveformBytesPerEntry );
4099- displayedWaveformTrackId = wfTrackId;
41004124 }
41014125 }
4102- else if (wfTrackId != 0 && displayedWaveformTrackId == 0 )
4126+ else if (wfTrackId != 0 && !waveformDisplay. hasWaveformData () )
41034127 {
41044128 // Waveform not yet loaded -- retry from cache (async: arrives after metadata)
41054129 juce::String pdlIP = sharedProDJLinkInput.getPlayerIP (pdlPlayer);
@@ -4108,7 +4132,6 @@ void MainComponent::timerCallback()
41084132 {
41094133 waveformDisplay.setColorWaveformData (meta.waveformData ,
41104134 meta.waveformEntryCount , meta.waveformBytesPerEntry );
4111- displayedWaveformTrackId = wfTrackId;
41124135 }
41134136 }
41144137 else if (wfTrackId == 0 && displayedWaveformTrackId != 0 )
0 commit comments