From 83532e17401dc2fdd68218aeb64163a542b21229 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 May 2026 22:53:23 +0000 Subject: [PATCH 1/2] Re-enable remote loop visuals with throttled updates Remove the SetVisualUpdatesEnabled(false) workaround from issue #68. Remote loop visual updates are now re-enabled but throttled to 15 Hz to prevent excessive decimation/upload cost. Changes: - LoopRemote::Update() now rate-limits visual model refreshes using steady_clock, re-setting the dirty flag when throttled so updates are retried on the next tick. - StationRemote::SetRemoteInterval() early-outs when length, visual length, and position are all unchanged, avoiding redundant buffer resize and dirty-flag work on each job tick. - Added _intervalVisualLengthSamps to StationRemote for the change guard. The existing vertex-shader architecture (fixed geometry + 1D waveform texture + GPU displacement) means no geometry rebuild is needed per update - only waveform decimation and PBO texture upload. Relates to #68 --- JammaLib/src/engine/LoopRemote.cpp | 18 +++++++++++++----- JammaLib/src/engine/LoopRemote.h | 10 ++++++++++ JammaLib/src/engine/StationRemote.cpp | 19 ++++++++++++++++--- JammaLib/src/engine/StationRemote.h | 1 + 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/JammaLib/src/engine/LoopRemote.cpp b/JammaLib/src/engine/LoopRemote.cpp index d1c351e4..e1926f53 100644 --- a/JammaLib/src/engine/LoopRemote.cpp +++ b/JammaLib/src/engine/LoopRemote.cpp @@ -9,15 +9,14 @@ LoopRemote::LoopRemote(LoopParams params, _modelDirty(false), _measureLengthSamps(0u), _measurePositionSamps(0u), - _visualLengthSamps(0u) + _visualLengthSamps(0u), + _lastVisualUpdate(std::chrono::steady_clock::now()) { SetMeasureLength(constants::DefaultSampleRate); SetVisualLength(constants::DefaultSampleRate); SetMeasurePosition(0u); - // Render the remote loop once so something is visible, then keep further - // remote visual updates disabled while the slowdown issue is investigated. + // Render the remote loop once so something is visible. _ForceUpdateLoopModel(); - SetVisualUpdatesEnabled(false); } void LoopRemote::SetVisualLength(unsigned int visualLengthSamps) @@ -35,13 +34,22 @@ void LoopRemote::Update() if (!_modelDirty.exchange(false)) return; - if (!_visualUpdatesEnabled) + // Throttle visual model updates to _MaxVisualUpdateHz to avoid excessive + // decimation/upload cost when multiple remote users are active. + // The vertex shader handles displacement from the waveform texture so + // no geometry rebuild is needed, but decimation is still O(loopLength). + auto now = std::chrono::steady_clock::now(); + auto elapsed = std::chrono::duration(now - _lastVisualUpdate).count(); + if (elapsed < (1.0 / _MaxVisualUpdateHz)) { + // Re-set the dirty flag so the update is retried next tick. + _modelDirty.store(true); _bufferBank.UpdateCapacity(); _monitorBufferBank.UpdateCapacity(); return; } + _lastVisualUpdate = now; Loop::Update(); } diff --git a/JammaLib/src/engine/LoopRemote.h b/JammaLib/src/engine/LoopRemote.h index 73ef07e4..ad724a5b 100644 --- a/JammaLib/src/engine/LoopRemote.h +++ b/JammaLib/src/engine/LoopRemote.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "Loop.h" namespace engine @@ -8,6 +9,11 @@ namespace engine // A loop that receives audio streamed in from a remote ninjam user. // Audio is written into the loop's BufferBank so it can be played back // and visualised through the standard Loop playback path. + // + // Visual model updates are throttled to a maximum rate to avoid excessive + // CPU cost when many remote users are active. The waveform is rendered via + // vertex-shader displacement from a 1D texture, so the per-update cost is + // limited to decimation + texture upload (no geometry rebuild). class LoopRemote : public Loop { @@ -38,5 +44,9 @@ namespace engine std::atomic _measureLengthSamps; std::atomic _measurePositionSamps; std::atomic _visualLengthSamps; + + // Throttle: visual updates are limited to _MaxVisualUpdateHz. + static constexpr double _MaxVisualUpdateHz = 15.0; + std::chrono::steady_clock::time_point _lastVisualUpdate; }; } diff --git a/JammaLib/src/engine/StationRemote.cpp b/JammaLib/src/engine/StationRemote.cpp index 7b0d464a..bac0dda2 100644 --- a/JammaLib/src/engine/StationRemote.cpp +++ b/JammaLib/src/engine/StationRemote.cpp @@ -13,6 +13,7 @@ StationRemote::StationRemote(StationParams params, _assignedOutputChannel(0u), _isConnectedRemote(false), _intervalLengthSamps(constants::DefaultSampleRate), + _intervalVisualLengthSamps(constants::DefaultSampleRate), _intervalPositionSamps(0u), _remoteTake(nullptr), _leftLoop(nullptr), @@ -120,21 +121,33 @@ void StationRemote::SetRemoteInterval(unsigned int lengthSamps, unsigned int pos { const auto safeLength = std::max(1u, lengthSamps); const auto safeVisualLength = std::max(safeLength, visualLengthSamps); + const auto safePosition = positionSamps % safeLength; + + // Early-out if nothing has changed (avoids redundant buffer + // resize / dirty-flag work on each job tick). + if (safeLength == _intervalLengthSamps.load() && + safeVisualLength == _intervalVisualLengthSamps.load() && + safePosition == _intervalPositionSamps.load()) + { + return; + } + _intervalLengthSamps.store(safeLength); - _intervalPositionSamps.store(positionSamps % safeLength); + _intervalVisualLengthSamps.store(safeVisualLength); + _intervalPositionSamps.store(safePosition); if (_leftLoop) { _leftLoop->SetMeasureLength(safeLength); _leftLoop->SetVisualLength(safeVisualLength); - _leftLoop->SetMeasurePosition(_intervalPositionSamps.load()); + _leftLoop->SetMeasurePosition(safePosition); } if (_rightLoop) { _rightLoop->SetMeasureLength(safeLength); _rightLoop->SetVisualLength(safeVisualLength); - _rightLoop->SetMeasurePosition(_intervalPositionSamps.load()); + _rightLoop->SetMeasurePosition(safePosition); } } diff --git a/JammaLib/src/engine/StationRemote.h b/JammaLib/src/engine/StationRemote.h index ce4d3429..daa4af6e 100644 --- a/JammaLib/src/engine/StationRemote.h +++ b/JammaLib/src/engine/StationRemote.h @@ -58,6 +58,7 @@ namespace engine bool _isConnectedRemote; std::atomic _intervalLengthSamps; + std::atomic _intervalVisualLengthSamps; std::atomic _intervalPositionSamps; std::shared_ptr _remoteTake; From ff272e9a5039411880af87f0d8563984c59f80e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 31 May 2026 22:57:24 +0000 Subject: [PATCH 2/2] Address review: check throttle before consuming dirty flag Restructure LoopRemote::Update() to check the time throttle before clearing _modelDirty, eliminating the exchange-then-re-set pattern that could theoretically race. Also use explicit types per review. --- JammaLib/src/engine/LoopRemote.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/JammaLib/src/engine/LoopRemote.cpp b/JammaLib/src/engine/LoopRemote.cpp index e1926f53..82be34f7 100644 --- a/JammaLib/src/engine/LoopRemote.cpp +++ b/JammaLib/src/engine/LoopRemote.cpp @@ -31,24 +31,25 @@ void LoopRemote::SetVisualLength(unsigned int visualLengthSamps) void LoopRemote::Update() { - if (!_modelDirty.exchange(false)) + if (!_modelDirty.load()) return; // Throttle visual model updates to _MaxVisualUpdateHz to avoid excessive // decimation/upload cost when multiple remote users are active. // The vertex shader handles displacement from the waveform texture so // no geometry rebuild is needed, but decimation is still O(loopLength). - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration(now - _lastVisualUpdate).count(); + std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); + double elapsed = std::chrono::duration(now - _lastVisualUpdate).count(); if (elapsed < (1.0 / _MaxVisualUpdateHz)) { - // Re-set the dirty flag so the update is retried next tick. - _modelDirty.store(true); + // Leave _modelDirty set so the update is retried next tick. _bufferBank.UpdateCapacity(); _monitorBufferBank.UpdateCapacity(); return; } + // Consume the dirty flag now that we will perform the update. + _modelDirty.store(false); _lastVisualUpdate = now; Loop::Update(); }