@@ -329,16 +329,15 @@ class alignas(64) Oscilloscope : public juce::Thread
329329
330330 backImage = images[(size_t )currentFront].createCopy ();
331331
332- // Left scrolling 2.0: Hopefully without crashing host DAWs
332+ // Left scrolling 2.0: Thread-safe pixel copy to avoid Direct2D races on
333+ // Windows
333334 const int shift = jmin (pixelToDraw, width);
334335 if (shift > 0 ) {
335- // Move content LEFT safely inside bounds
336- // backImage.moveImageSection(0,
337- // 0, // destX, destY
338- // shift,
339- // 0, // sourceX, sourceY
340- // width - shift,
341- // height);
336+ // Use BitmapData for guaranteed thread-safe, platform-agnostic pixel
337+ // operations. On Windows with Direct2D images, BitmapData access forces
338+ // software conversion, which is safe from worker threads. On macOS/Linux,
339+ // it accesses native format directly.
340+ scrollImageSectionLeft (backImage, shift, width, height);
342341
343342 backImage.clear (juce::Rectangle<int >(width - shift, 0 , shift, height),
344343 juce::Colours::transparentBlack);
@@ -370,6 +369,55 @@ class alignas(64) Oscilloscope : public juce::Thread
370369
371370 // ==============================================================================
372371private:
372+ // ==============================================================================
373+ /* *
374+ * @brief Thread-safe pixel scroll operation for worker threads.
375+ *
376+ * @details
377+ * Manually copies pixels left by shiftAmount using BitmapData,
378+ * avoiding Direct2D's moveImageSection() which is not thread-safe
379+ * when called from worker threads on Windows.
380+ *
381+ * On Direct2D-backed images, accessing BitmapData forces a software
382+ * conversion, making this operation safe from any thread.
383+ * On software images, this is just a fast memcpy.
384+ */
385+ static void scrollImageSectionLeft (Image& image,
386+ int shiftAmount,
387+ int width,
388+ int height) noexcept
389+ {
390+ if (shiftAmount <= 0 || width <= 0 || height <= 0 )
391+ return ;
392+
393+ const int srcX = shiftAmount;
394+ const int copyWidth = width - shiftAmount;
395+
396+ try {
397+ // Read-only access from source region
398+ const Image::BitmapData srcData (
399+ image, srcX, 0 , copyWidth, height, Image::BitmapData::readOnly);
400+ // Write access to destination region
401+ const Image::BitmapData dstData (
402+ image, 0 , 0 , copyWidth, height, Image::BitmapData::readWrite);
403+
404+ // Verify format consistency
405+ if (srcData.pixelFormat != dstData.pixelFormat ||
406+ srcData.pixelStride != dstData.pixelStride )
407+ return ;
408+
409+ // Copy each scanline
410+ const size_t lineSize = (size_t )dstData.pixelStride * (size_t )copyWidth;
411+ for (int y = 0 ; y < height; ++y) {
412+ std::memcpy (
413+ dstData.getLinePointer (y), srcData.getLinePointer (y), lineSize);
414+ }
415+ } catch (const std::exception&) {
416+ // BitmapData access failed; gracefully skip scroll
417+ // (better than crashing from Direct2D race)
418+ }
419+ }
420+
373421 // ==============================================================================
374422 // Members initialized in the initializer list
375423 RingBuffer& ringBuffer;
0 commit comments