diff --git a/CHANGELOG.md b/CHANGELOG.md index d1da562..e1ccb93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -- **Player frame-time hotspots removed (trace-backed) + weak-hardware hardening.** A Chrome performance trace of a 3D-highway session surfaced two core per-frame layout-thrash sources, now fixed: the highway's visibility check read `canvas.offsetParent` every rAF frame (forces style/layout recalc — now sampled every 10th frame with a cached value, force-refreshed on init/canvas-replace/resize/override-clear), and the v3 player chrome loop called `matches(':hover')` per frame and unconditionally rewrote the Up-Next pill's `textContent`/bar width at 6 Hz (now hover-tracked via mouseenter/mouseleave, DOM writes only on value change, progress bar moved from `width` to compositor-only `scaleX`). The 3D highway pre-warms shader programs (`ren.compile`) and deterministic label textures at init — and chart-dependent chord/section label textures on first draw — so first-appearance shader-compile/texture-upload frame spikes move into the load spinner. For weaker hardware: the per-frame renderer bundle is now a single reused object instead of a fresh ~35-field allocation per frame (object identity is stable and meaningless; array fields still swap reference on chart changes), custom viz get `bundle.lowerBoundT`/`bundle.lowerBoundTime` binary-search helpers for visible-window culling, the default 2D highway's beat lines no longer scan every beat in the song per frame, and the 3D highway stops reading `localStorage` per frame (1 Hz poll) and caches its lyrics text-measurement layout per displayed line instead of re-measuring every syllable every frame. A second, throttled-CPU trace pass additionally removed: shader-program re-resolution churn from label texture swaps (`material.needsUpdate` is now only set on a null↔texture transition — swapping between two cached label textures never changes the compiled program), the 3D highway's per-frame `getBoundingClientRect` layout read in its canvas-size self-check (now every 10th frame, still immediate on backing-store change), and the core 60 Hz HUD clock rewriting `textContent` on every tick (now write-on-change, ~1/s). +- **Player frame-time hotspots removed (trace-backed) + weak-hardware hardening.** A Chrome performance trace of a 3D-highway session surfaced two core per-frame layout-thrash sources, now fixed: the highway's visibility check read `canvas.offsetParent` every rAF frame (forces style/layout recalc — now sampled every 10th frame with a cached value, force-refreshed on init/canvas-replace/resize/override-clear), and the v3 player chrome loop called `matches(':hover')` per frame and unconditionally rewrote the Up-Next pill's `textContent`/bar width at 6 Hz (now hover-tracked via mouseenter/mouseleave, DOM writes only on value change, progress bar moved from `width` to compositor-only `scaleX`). The 3D highway pre-warms shader programs (`ren.compile`) and deterministic label textures at init — and chart-dependent chord/section label textures on first draw — so first-appearance shader-compile/texture-upload frame spikes move into the load spinner. For weaker hardware: the per-frame renderer bundle is now a single reused object instead of a fresh ~35-field allocation per frame (object identity is stable and meaningless; array fields still swap reference on chart changes), custom viz get `bundle.lowerBoundT`/`bundle.lowerBoundTime` binary-search helpers for visible-window culling, the default 2D highway's beat lines no longer scan every beat in the song per frame, and the 3D highway stops reading `localStorage` per frame (1 Hz poll) and caches its lyrics text-measurement layout per displayed line instead of re-measuring every syllable every frame. A second, throttled-CPU trace pass additionally removed: shader-program re-resolution churn from label texture swaps (`material.needsUpdate` is now only set on a null↔texture transition — swapping between two cached label textures never changes the compiled program), the 3D highway's per-frame `getBoundingClientRect` layout read in its canvas-size self-check (now every 10th frame, still immediate on backing-store change), and the core 60 Hz HUD clock rewriting `textContent` on every tick (now write-on-change, ~1/s). The dominant residual — steady `getParameters` shader-program re-resolution (~4% of throttled main thread) — turned out to be Three r158+'s transparent-DoubleSide two-pass rendering, which sets `material.needsUpdate` twice per object per frame; all 18 of the 3D highway's transparent DoubleSide materials are flat unlit quads (labels, rails, chord frames, lanes), so they now declare `forceSinglePass: true`, eliminating the recompile churn and halving those objects' draw calls. ### Added - **The tuner now tracks what tuning your instrument is *actually* in, so it prompts you to retune in BOTH directions — down to a song's tuning, and back up when the next song needs it.** The coverage check used to compare each song against your fixed instrument-profile tuning, so it only ever prompted you *away* from "home" (e.g. E → Drop C#) and stayed silent coming back (Drop C# → E), even though you'd physically retuned. It now reads the host's live **per-instrument working tuning** (`window.feedBack.workingTuning`) — what your selected instrument is currently in — so coverage is measured against your *actual* tuning and fires both ways. When you clear an auto-opened tuner, the tuner publishes that song's tuning as your instrument's live working tuning (`assumed` — an explicit "I tuned / Skip" refines it in a later PR), so the next song is judged against where you now are. **Per-instrument** — your guitar's and bass's tunings are tracked separately (keyed like the selector), so switching instruments uses the right one. Feature-detected: on a host without the working-tuning capability it falls back to the static `/api/settings` tuning (today's behavior). `plugins/tuner/screen.js` (`_playerTuning` reads `workingTuning` keyed by the selected instrument; `_publishWorkingTuning` writes on clear). Builds on the host `workingTuning` foundation (PR 1 of the series) + the instrument→chart routing (PR 2). Tests: `tests/js/tuner_auto_open.test.js` (both-directions coverage via a live Drop-D working tuning; publish-on-clear targets the right instrument slot) — 29 pass. diff --git a/plugins/highway_3d/plugin.json b/plugins/highway_3d/plugin.json index c394b44..8c9c2f7 100644 --- a/plugins/highway_3d/plugin.json +++ b/plugins/highway_3d/plugin.json @@ -1,7 +1,7 @@ { "id": "highway_3d", "name": "3D Highway", - "version": "3.31.1", + "version": "3.31.2", "type": "visualization", "bundled": true, "script": "screen.js", diff --git a/plugins/highway_3d/screen.js b/plugins/highway_3d/screen.js index fd9249f..0346614 100644 --- a/plugins/highway_3d/screen.js +++ b/plugins/highway_3d/screen.js @@ -5278,7 +5278,17 @@ // appearing on top without depthTest. depthTest: false, depthWrite: false, - side: T.DoubleSide, + // forceSinglePass accompanies EVERY transparent DoubleSide + // material in this file: without it, Three r158+ renders + // each such object in TWO passes (back side then front), + // setting material.needsUpdate on both — which forces a + // full getParameters/program-cache lookup per object per + // frame (profiled at ~4% of throttled main-thread time) + // and doubles the draw calls. The two-pass path exists to + // fix self-occlusion sorting on closed transparent meshes; + // all our DoubleSide materials are flat unlit quads + // (labels, rails, frames, lanes) where it buys nothing. + side: T.DoubleSide, forceSinglePass: true, }); sm.userData.h3dTechMeshMat = base; } @@ -6660,7 +6670,7 @@ depthWrite: false, depthTest: true, blending: T.AdditiveBlending, - side: T.DoubleSide, + side: T.DoubleSide, forceSinglePass: true, fog: true, })); mAccentHaloNear = mkAccentHaloMats(ACCENT_HALO_OP_NEAR); @@ -6720,7 +6730,7 @@ new T.MeshBasicMaterial({ vertexColors: true, transparent: true, opacity: 1.0, depthWrite: false, - blending: T.AdditiveBlending, side: T.DoubleSide, fog: false, + blending: T.AdditiveBlending, side: T.DoubleSide, forceSinglePass: true, fog: false, }), )); // Notedetect feedback outline (issue #9): hot magenta-red (0xff0066, hue @@ -6860,7 +6870,7 @@ emissiveIntensity: 0.9, transparent: true, opacity: 0.85, - side: T.DoubleSide, + side: T.DoubleSide, forceSinglePass: true, depthWrite: false, depthTest: false, }); @@ -6887,7 +6897,7 @@ color: CHORD_BOX_TEAL_HEX, transparent: true, opacity: 0.85, depthTest: false, depthWrite: false, - fog: false, side: T.DoubleSide, + fog: false, side: T.DoubleSide, forceSinglePass: true, }); pSusRail = pool(noteG, () => { const m = new T.Mesh(gSusRail, mSusRailBase.clone()); @@ -6908,7 +6918,7 @@ transparent: true, opacity: 0.55, blending: T.AdditiveBlending, depthTest: false, depthWrite: false, - fog: false, side: T.DoubleSide, + fog: false, side: T.DoubleSide, forceSinglePass: true, }); pSusRailBloom = pool(noteG, () => { const m = new T.Mesh(gSusRailBloom, mSusRailBloomBase.clone()); @@ -6922,7 +6932,7 @@ gTechPlane = new T.PlaneGeometry(1, 1); pTechPlane = pool(noteG, () => { const m = new T.Mesh(gTechPlane, new T.MeshBasicMaterial({ - transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide, + transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide, forceSinglePass: true, })); m.renderOrder = 1000; return m; @@ -6976,7 +6986,7 @@ uniforms: { map: { value: spriteMat.map } }, vertexShader: _imTechVert, fragmentShader: _imTechFrag, - transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide, + transparent: true, depthTest: false, depthWrite: false, side: T.DoubleSide, forceSinglePass: true, }); const im = new T.InstancedMesh(geo, mat, IM_TECH_CAP); im.instanceMatrix.setUsage(T.DynamicDrawUsage); @@ -7089,7 +7099,7 @@ depthWrite: false, depthTest: false, fog: false, - side: T.DoubleSide, + side: T.DoubleSide, forceSinglePass: true, }), )); pChordBox = pool(noteG, () => new T.Mesh( @@ -7101,7 +7111,7 @@ depthWrite: false, depthTest: false, fog: false, - side: T.DoubleSide, + side: T.DoubleSide, forceSinglePass: true, }), )); @@ -7184,7 +7194,7 @@ _imPMXFillMat = new T.ShaderMaterial({ vertexShader: _imFillVert, fragmentShader: _imFillFrag, transparent: true, depthTest: false, depthWrite: false, - fog: false, side: T.DoubleSide, + fog: false, side: T.DoubleSide, forceSinglePass: true, }); imPMXFill = new T.InstancedMesh(gPMXFill, _imPMXFillMat, IM_STRUM_CAP); imPMXFill.instanceMatrix.setUsage(T.DynamicDrawUsage); @@ -7264,7 +7274,7 @@ _imFHXFillMat = new T.ShaderMaterial({ vertexShader: _imFillVert, fragmentShader: _imFillFrag, transparent: true, depthTest: false, depthWrite: false, - fog: false, side: T.DoubleSide, + fog: false, side: T.DoubleSide, forceSinglePass: true, }); imFHXFill = new T.InstancedMesh(gFHXFill, _imFHXFillMat, IM_STRUM_CAP); imFHXFill.instanceMatrix.setUsage(T.DynamicDrawUsage); @@ -7345,7 +7355,7 @@ _imPMXLinesMat = new T.ShaderMaterial({ vertexShader: _imLinesVert, fragmentShader: _imLinesFrag, transparent: true, depthTest: false, depthWrite: false, - fog: false, side: T.DoubleSide, + fog: false, side: T.DoubleSide, forceSinglePass: true, }); imPMXLines = new T.InstancedMesh(gPMXLines, _imPMXLinesMat, IM_STRUM_CAP); imPMXLines.instanceMatrix.setUsage(T.DynamicDrawUsage); @@ -7427,7 +7437,7 @@ _imFHXLinesMat = new T.ShaderMaterial({ vertexShader: _imLinesVert, fragmentShader: _imLinesFrag, transparent: true, depthTest: false, depthWrite: false, - fog: false, side: T.DoubleSide, + fog: false, side: T.DoubleSide, forceSinglePass: true, }); imFHXLines = new T.InstancedMesh(gFHXLines, _imFHXLinesMat, IM_STRUM_CAP); imFHXLines.instanceMatrix.setUsage(T.DynamicDrawUsage); @@ -7449,28 +7459,28 @@ gPMXFill, new T.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 1, - depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, + depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true, }), )); pFHXFill = pool(noteG, () => new T.Mesh( gFHXFill, new T.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 1, - depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, + depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true, }), )); pMuteXLines = pool(noteG, () => new T.Mesh( gPMXLines, new T.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 1, - depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, + depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true, }), )); pFHXLines = pool(noteG, () => new T.Mesh( gFHXLines, new T.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 1, - depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, + depthWrite: false, depthTest: false, fog: false, side: T.DoubleSide, forceSinglePass: true, }), )); @@ -9935,12 +9945,14 @@ // Swap a pooled label sprite's cached texture WITHOUT recompiling. // Setting material.needsUpdate bumps material.version, which forces // Three.js through getParameters/getProgramCacheKey on the next - // render — profiled at ~4% of throttled main-thread time from the - // per-frame label map swaps in dense charts. Swapping one non-null - // texture for another does NOT change the compiled program (the - // USE_MAP define is unchanged); only a null <-> non-null transition - // does, and pooled label sprites are constructed with a non-null - // map, so in practice this never recompiles. + // render. Swapping one non-null texture for another does NOT change + // the compiled program (the USE_MAP define is unchanged); only a + // null <-> non-null transition does, and pooled label sprites are + // constructed with a non-null map, so in practice this never + // recompiles. (Note: the DOMINANT getParameters churn turned out to + // be Three's transparent-DoubleSide two-pass path — see the + // forceSinglePass comment in _spriteMat2MeshMat — this helper + // removes the label-swap contribution on top of that.) function _setLabelMap(sprite, srcMat) { const m = sprite.material; if (m.map === srcMat.map) return;