Squash-merged diag/feeder work already in main; conflict was the
deferred install-line gating vs main's plain flag resolve — kept ours.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The error tap and install lines from the previous diag commit were
unconditional. Now: error/rejection taps check _asioDiagEnabled() at
event time; install lines log deferred once the async debugEnabled()
resolves true. The NOT-installed anomaly lines stay bridge-gated
(window.feedBackDesktop present) instead — a broken bridge can't deliver
the debug flag, they fire at most once, and only in the broken state
they exist to witness. Docker sphere: fully silent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Without Cache-Control Chromium's heuristic freshness (10% of file age)
serves /static/app.js from disk cache for hours-to-days without
revalidating. Desktop consequence: a new build's window ran the previous
build's app.js — the 2026-07-11 ASIO investigation traced 'routing
watcher never installed' + a stems module-plugin SyntaxError to exactly
this (stale loader predating scriptType support). no-cache keeps caching
but revalidates via ETag — unchanged files still cost only a 304.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 tester log showed the routing watcher and renderer-bus feeder
never installed (zero [feedpak-route]/[renderer-bus] lines) plus an
uncaught SyntaxError with no source location — nothing in the log said
why. New:
- global error/unhandledrejection tap logging message + filename:line:col
(error events carry the location even for parse errors in other scripts)
- explicit install / NOT-installed lines for watcher and feeder (incl.
loopback capability probe)
- DOMException detail (name/message/stack head) in the feeder retry warn
— the console-message forward stringified it to [object DOMException]
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tester-confirmed (2026-07-11 log): song previews and other
plugin-private audio bypass the per-surface feeder taps and leak to the
default WASAPI device under ASIO output. Also confirmed: the element
capture path poisons itself when highway_3d already owns #audio's
one-shot MediaElementSource (InvalidStateError with _elCtx assigned
pre-throw → TypeError every later tick).
- New preferred mode 'loopback': one getDisplayMedia frame-audio capture
(desktop main answers with the app's own frame) covers song, previews,
and UI sounds for the whole exclusive session — engages even with no
song loaded. Local playback silenced via suppressLocalAudioPlayback,
page-mute IPC fallback otherwise.
- Sticky fallback to the existing stems/element surface modes when
capture is unavailable (old desktop main, denied, Docker sphere).
- Element capture: assign module state only after the whole chain
succeeds; close the context on failure — collision now retries clean.
- Failed engage now disables the bus and tears down loopback (no more
bus-enabled-with-no-producer stranding).
- Tests: 12 (5 new — loopback engage/preference/mute-fallback/sticky
fallback, collision retry).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tester-confirmed (2026-07-11 log): song previews and other
plugin-private audio bypass the per-surface feeder taps and leak to the
default WASAPI device under ASIO output. Also confirmed: the element
capture path poisons itself when highway_3d already owns #audio's
one-shot MediaElementSource (InvalidStateError with _elCtx assigned
pre-throw → TypeError every later tick).
- New preferred mode 'loopback': one getDisplayMedia frame-audio capture
(desktop main answers with the app's own frame) covers song, previews,
and UI sounds for the whole exclusive session — engages even with no
song loaded. Local playback silenced via suppressLocalAudioPlayback,
page-mute IPC fallback otherwise.
- Sticky fallback to the existing stems/element surface modes when
capture is unavailable (old desktop main, denied, Docker sphere).
- Element capture: assign module state only after the whole chain
succeeds; close the context on failure — collision now retries clean.
- Failed engage now disables the bus and tears down loopback (no more
bus-enabled-with-no-producer stranding).
- Tests: 12 (5 new — loopback engage/preference/mute-fallback/sticky
fallback, collision retry).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
main already contains the renderer-bus Phase 2 feeder; conflicts were
the [asio-diag] insertions vs identical upstream context — kept ours.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gated on window.feedBackDesktop.audio.debugEnabled() (desktop --debug);
inert in the Docker sphere and normal desktop runs.
- [asio-diag] getCurrentDevice= full device object on outputType change
(catches ASIO drivers reporting a non-'ASIO' type name)
- [asio-diag] renderer-bus: full feeder decision vector, change-gated
(running/exclusive/stems/juceMode/elementSong/want/mode)
- [asio-diag] setSink: every sink flip with ctx state + rate
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under exclusive-style output the native backing transport (Phase 1, #824)
carries loose /audio/ songs and feedpak full-mixes, but not the stems
plugin's multi-stem WebAudio graph or tracks JUCE rejected. The feeder taps
the renderer-side master with an AudioWorklet, re-points the owning
AudioContext at a null sink so it keeps rendering without a device, and
pushes ~10 ms chunks over IPC into the desktop engine's renderer bus
(feedBack-desktop#90 follow-up). Inert in the Docker sphere and in shared
mode. Validated by the fix12 tester spike: null-sink rendering works,
clocks hold, no overflow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Song playback runs through the renderer, which WASAPI-exclusive (and
ASIO) output silences. Route single-mix feedpaks (stem-less
original_audio packs AND single-stem packs) onto the engine's backing
transport when the output device type is exclusive-style, and migrate
back to HTML5 when it isn't. Extends /api/audio-local-path to resolve
/api/sloppak/.../file/... URLs via the same containment guards as
serve_sloppak_file. Multi-stem packs stay on the WebAudio path
(Phase 2). Includes [feedpak-route] transition-gated diagnostics
logging.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previously pinned BtbN autobuild release (2026-06-19) was pruned
upstream, so the release build's curl download 404'd (exit 22).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Off state had a 1px border, on state none — toggling grew/shrank the
button 2px and shifted the row. On state now carries a same-color
border (border-fb-primary, already in the prebuilt CSS).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
w-4 icon (16px) vs text-sm line-height (20px) made the shuffle button
4px shorter than its neighbor at equal py-2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Crossing-arrows toggle next to Play all / Play album on the playlist
detail page. When on, playQueue.start Fisher-Yates-shuffles the queue
once at start — on a copy, so the stored playlist order is untouched —
swapping per-slot album arrangements in lockstep so each slot keeps its
pinned arrangement (#685 contract preserved). Prev-less queue semantics
are unchanged: auto-advance simply walks the shuffled order.
Preference is global, persisted as localStorage v3PlaylistShuffle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The retrace after the label-swap fix showed getParameters unchanged
(~2.5s / ~4% throttled main thread) — the real driver is Three r158+'s
transparent-DoubleSide two-pass path: renderBufferDirect renders such
objects back side then front side, setting material.needsUpdate BOTH
times, i.e. a full getParameters/program-cache lookup twice per object
per frame, plus double draw calls. (Found by reading the two-pass
branch in the vendored three.module.min.js right next to the
getParameters call site.)
All 18 transparent DoubleSide materials in this renderer are flat
unlit quads — technique markers, sustain rails, chord frames, lane
planes, halo bars — where the two-pass self-occlusion ordering buys
nothing. Declare forceSinglePass: true on all of them.
Also corrects the _setLabelMap comment's churn attribution (that fix
removes the label-swap contribution; this one removes the dominant
source). Plugin 3.31.1 -> 3.31.2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A 4x-CPU-throttled retrace (the honest weak-hardware proxy) surfaced
three residual per-frame costs; stack attribution pinned each:
- getParameters/getProgramCacheKey (~4% of main thread): every pooled
label sprite map swap set material.needsUpdate, bumping
material.version and forcing full program re-resolution next render.
Swapping between two non-null cached textures never changes the
compiled program (USE_MAP define unchanged) — new _setLabelMap()
helper only flags needsUpdate on a null<->texture transition, used at
all 7 swap sites.
- getBoundingClientRect (~1.2%): the 3D highway's per-frame canvas-size
self-check forced a layout read every frame. The CSS-box drift read
now runs every 10th frame (or when the wrap isn't pinned); the
backing-store comparison stays per-frame with cheap property reads
and forces an immediate box read + applySize when it fires.
- set textContent: the core 60 Hz HUD clock rewrote hud-time (and
getElementById'd it) every tick for a display that changes 1/s — now
write-on-change with a cached element ref.
(The remaining textContent writer in the trace is notedetect's
badges.js — external repo, to be filed there.)
tests/js: resize-reframe shape test updated for the hoisted _bsChanged
gate, incl. an assertion that the throttle can never delay the
backing-store path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- isVisible() forces a fresh DOM sample (was serving the rAF loop's
throttled cache, contradicting its 'live DOM check' docstring).
- v3 chrome: reconcile the edge-driven overControls hover flag against
matches(':hover') on the throttled ~6 Hz tick — covers a missed
mouseleave (flag stuck true, transport never hides) and a re-created
#player-controls node with lost listeners.
- highway_3d pre-warm now also covers teachFg/teachSd label textures
and the technique sprite factories (mute X, hammer/pull triangles,
bend chevrons, slide arrows) per active-palette string colour, plus
a maintenance note tying new label styles to the warm list.
- Document that the visibility throttle's manual invalidations are
latency-only (periodic resample self-heals within ~10 frames), and
why highway_3d keeps its local lowerBoundT (downlevel hosts).
External-repo audit (finding 1): staffview, tabview, piano, drums,
keys_highway_3d, drum_highway_3d grepped — no cross-frame bundle
retention or bundle-identity checks found.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Static-analysis follow-ups to the trace-backed fixes; each is cheap
insurance on machines where the profiled headroom doesn't exist.
- highway.js: _makeBundle now mutates one persistent per-instance
object instead of allocating a fresh ~35-field bundle every rAF
frame (xN under splitscreen). Object identity is stable and
meaningless; array fields still swap reference on chart changes,
which field-identity caches rely on. Contract documented in both
CLAUDE.mds.
- highway.js: new bsearchTime (lower-bound on .time) windows the
default 2D renderer's beat-line scan (was O(all beats) per frame);
bundle.lowerBoundT / bundle.lowerBoundTime expose the searches to
custom viz so they stop reimplementing visible-window culling.
- highway_3d: localStorage 'h3d_full_sus' polled at ~1 Hz instead of
every frame (synchronous storage read on the hot path).
- highway_3d: drawLyrics caches the measureText row layout keyed on
(lyrics ref, line index, shown count, font size, width) — per-frame
work is now just drawing over cached widths.
- tests/js: bundle source-shape assertions widened to accept the
assignment form ([:=]) alongside the old object-literal form.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Trace showed frame spikes from Three.js first-use costs mid-song:
shader program compilation (getParameters/getProgramCacheKey) and lazy
texture uploads (texSubImage2D) whenever a chord name, section banner,
or fret label first appeared.
- ren.compile(scene, cam) after initScene (pools already warmed by
feedBack#226, board built, background mounted) so programs compile
during the load spinner.
- Pre-rasterise + GPU-upload (ren.initTexture) the deterministic txtMat
entries: fret numbers 0-24 in the noteFret/fretRow/ghostFret combos
the per-frame paths request.
- Chart-dependent labels (chord template names, section names) prewarm
once on the first draw() after each init, when bundle arrays are
guaranteed populated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chrome trace showed ~0.5s self-time in _isHighwayVisible (offsetParent
read every rAF frame forces style/layout recalc) and ~1.5s in the v3
player-chrome loop (matches(':hover') per frame, unconditional
textContent/width writes at 6 Hz -> ~1800 layout passes in 63s).
- highway.js: sample offsetParent every 10th frame, cached in between;
fresh sample forced on init/canvas-replace/resize/override-clear.
- player-chrome.js: hover tracked via mouseenter/mouseleave; Up-Next
refs cached, text written only on change (eta coarsened to 1s steps
beyond 10s), progress bar moved from width to scaleX (compositor-only).
- v3.css: bar fill uses transform-origin:left + scaleX transition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>