Restored from slopsmith/slopsmith#654 — original issue, opened by @byrongamatos on 2026-06-01.
[restored-from: slopsmith/slopsmith#654]
Summary
Several users — mainly on Windows, several with multi-monitor setups — report performance issues (stutter, fan spin-up, and in the desktop app audio dropouts) while on the player, especially with the 3D Highway visualization. The root cause is in core: the highway render loop runs unconditionally and is never gated on playback state, so an expensive renderer (3D Highway / Three.js WebGL) does a full scene render every animation frame even while playback is paused. Windows-specific browser/compositor behavior amplifies this, which is why it largely doesn't reproduce on Linux at 60 Hz.
Root cause — the render loop only gates visibility and ready, never playback
static/highway.js draw() (~line 1024) re-arms itself every frame and only skips work on two conditions:
function draw() {
animFrame = requestAnimationFrame(draw); // always re-arms
if (!canvas || !_renderer) return;
_emitVisibilityIfChanged();
if (!_lastVisible) return; // gate 1: offsetParent / setVisible override
if (!ready) return; // gate 2: WS ready
... _renderer.draw(bundle) ... // expensive 3D render
}
- The loop starts on the first
ready (~line 2965) and the only cancelAnimationFrame is in highway.stop() (~line 3244).
- There is no play/pause gate. While a song is loaded, the player screen is up, and the canvas is visible,
_renderer.draw(bundle) runs on every frame — including while paused.
- For the default 2D renderer this is cheap and went unnoticed. For the 3D Highway (
byrongamatos/slopsmith-plugin-3dhighway, host-driven draw() → ren.render(scene, cam)) it is a full WebGL scene render at the display's refresh rate, continuously.
Leaving the player is handled correctly: .screen { display:none } (static/style.css:13-14) trips offsetParent === null → _lastVisible false → draw bails, and highway.stop() is called on the stop paths. So the waste is specifically: on the player, song loaded, canvas visible, but not actively playing.
Why it hits Windows (and not Linux/60 Hz)
- Chromium paces rAF to the highest-refresh attached monitor (DWM-tied) regardless of which monitor the window is on. If any connected display is 120/144/165 Hz, the unconditional loop runs at that rate everywhere — 2–2.7× the work — even on a 60 Hz panel. This is the leading explanation for the platform split.
- WebGL on Windows goes through ANGLE → Direct3D 11, heavier and differently paced than native GL on Linux.
- DWM multi-monitor present/composite adds overhead and historically causes mixed/independent-vblank stutter.
- The desktop app is Electron (same Chromium engine) and shares the machine with the JUCE audio engine, so GPU contention surfaces as audio glitches.
Sync is not a concern for gating/throttling
Note position is clock-derived, not frame-accumulated: currentTime is the audio-aligned clock (highway.js:61-63, interpolated via performance.now() at 3054-3071) and notes are drawn as n.t - currentTime every frame (1456-1459, 1553, 1654). At any frame rate, a note is placed at the correct time-based position for the instant it's drawn. Gating while paused, or throttling frame rate / render resolution during playback, changes smoothness, never audio/visual sync.
Related: the visibility:hidden overlay gap
Core exposes window.slopsmith.highway.setVisible(true|false|null) (highway.js:~3100) precisely for hosts that hide the highway via visibility:hidden / opacity:0 (which offsetParent can't detect). Findings:
- No viz plugin calls
setVisible() today.
tabview hides the host canvas with visibility:hidden (screen.js:488) but never calls setVisible(false), so the underlying renderer (e.g. the 3D Highway's .h3d-wrap WebGL overlay) keeps rendering full-tilt behind the opaque tab view.
- Only
jumpingtab participates in the visibility protocol (listens to highway:visibility); the 3D Highway plugin has no listener to hide its .h3d-wrap.
Proposed fixes (separate PRs, all reference this issue)
- Core
slopsmith/slopsmith — playback pause gate in highway.js: skip _renderer.draw when the audio clock is stalled/paused, rendering one frame on pause→play / seek transitions so the static frame stays correct. Highest-impact, no-downside, helps every renderer.
slopsmith-plugin-3dhighway — adaptive _renderScale reduction under load (keeps native-refresh smoothness while cutting per-frame GPU cost — the better lever than an fps cap, avoids non-integer-refresh judder), a debug fps/frame-time readout, and a highway:visibility listener that hides .h3d-wrap when the host goes invisible.
slopsmith-plugin-tabview — call highway.setVisible(false) when hiding the host canvas and setVisible(null) on teardown/restore, so the gate trips and highway:visibility fires for overlay renderers.
How a reporter can confirm
- Report each monitor's actual refresh rate (Windows → Display → Advanced display); set all displays to 60 Hz or unplug the high-refresh one → does it clear up?
- Does it occur while paused, not just during playback? (confirms the unconditional loop)
chrome://gpu / about:support → compositing + vsync info; compare window.devicePixelRatio per monitor.
Summary
Several users — mainly on Windows, several with multi-monitor setups — report performance issues (stutter, fan spin-up, and in the desktop app audio dropouts) while on the player, especially with the 3D Highway visualization. The root cause is in core: the highway render loop runs unconditionally and is never gated on playback state, so an expensive renderer (3D Highway / Three.js WebGL) does a full scene render every animation frame even while playback is paused. Windows-specific browser/compositor behavior amplifies this, which is why it largely doesn't reproduce on Linux at 60 Hz.
Root cause — the render loop only gates visibility and ready, never playback
static/highway.jsdraw()(~line 1024) re-arms itself every frame and only skips work on two conditions:ready(~line 2965) and the onlycancelAnimationFrameis inhighway.stop()(~line 3244)._renderer.draw(bundle)runs on every frame — including while paused.byrongamatos/slopsmith-plugin-3dhighway, host-drivendraw()→ren.render(scene, cam)) it is a full WebGL scene render at the display's refresh rate, continuously.Leaving the player is handled correctly:
.screen { display:none }(static/style.css:13-14) tripsoffsetParent === null→_lastVisiblefalse → draw bails, andhighway.stop()is called on the stop paths. So the waste is specifically: on the player, song loaded, canvas visible, but not actively playing.Why it hits Windows (and not Linux/60 Hz)
Sync is not a concern for gating/throttling
Note position is clock-derived, not frame-accumulated:
currentTimeis the audio-aligned clock (highway.js:61-63, interpolated viaperformance.now()at3054-3071) and notes are drawn asn.t - currentTimeevery frame (1456-1459,1553,1654). At any frame rate, a note is placed at the correct time-based position for the instant it's drawn. Gating while paused, or throttling frame rate / render resolution during playback, changes smoothness, never audio/visual sync.Related: the
visibility:hiddenoverlay gapCore exposes
window.slopsmith.highway.setVisible(true|false|null)(highway.js:~3100) precisely for hosts that hide the highway viavisibility:hidden/opacity:0(whichoffsetParentcan't detect). Findings:setVisible()today.tabviewhides the host canvas withvisibility:hidden(screen.js:488) but never callssetVisible(false), so the underlying renderer (e.g. the 3D Highway's.h3d-wrapWebGL overlay) keeps rendering full-tilt behind the opaque tab view.jumpingtabparticipates in the visibility protocol (listens tohighway:visibility); the 3D Highway plugin has no listener to hide its.h3d-wrap.Proposed fixes (separate PRs, all reference this issue)
slopsmith/slopsmith— playback pause gate inhighway.js: skip_renderer.drawwhen the audio clock is stalled/paused, rendering one frame on pause→play / seek transitions so the static frame stays correct. Highest-impact, no-downside, helps every renderer.slopsmith-plugin-3dhighway— adaptive_renderScalereduction under load (keeps native-refresh smoothness while cutting per-frame GPU cost — the better lever than an fps cap, avoids non-integer-refresh judder), a debug fps/frame-time readout, and ahighway:visibilitylistener that hides.h3d-wrapwhen the host goes invisible.slopsmith-plugin-tabview— callhighway.setVisible(false)when hiding the host canvas andsetVisible(null)on teardown/restore, so the gate trips andhighway:visibilityfires for overlay renderers.How a reporter can confirm
chrome://gpu/ about:support → compositing + vsync info; comparewindow.devicePixelRatioper monitor.