mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 04:11:36 +00:00
Fix 3D drum/keys highways not resizing on fullscreen under splitscreen (#723)
The guitar/bass highway_3d renderer self-detects panel-canvas size changes in its draw() loop and re-runs applySize() every frame, because the splitscreen host overrides hw.resize and never calls renderer.resize(). The drum and keys highways lacked that fallback — they only re-framed when the host explicitly called resize(w, h) — so their panels stayed framed for the pre-fullscreen size while the guitar/bass panels adapted. Symptom: a too-small, off-center highway in the drum/keys panels after maximizing a split-screen session. Port highway_3d's per-frame drift check into both draw() loops: re-apply on backing-store change (canvas.width/height) AND on CSS-box drift (clientWidth/clientHeight vs the last applied logical size, throttled to every 10th frame). Track _lastHwW/_lastHwH + _appliedW/_appliedH per instance and reset them in destroy() so a reused instance re-frames on the next song. plugins/drum_highway_3d -> 0.3.1, plugins/keys_highway_3d -> 0.1.1. Tests: tests/js/drum_keys_highway_3d_resize_reframe.test.js. Signed-off-by: Kris Anderson <topkoa@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c7497c758d
commit
4b6cbe8b11
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **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.
|
||||
|
||||
### Fixed
|
||||
- **3D Drum & Keys highways now re-frame on fullscreen/layout drift under splitscreen.** The guitar/bass `highway_3d` self-detects when its panel canvas changes size and re-runs `applySize()` every frame, because the splitscreen host overrides `hw.resize` and never calls `renderer.resize()`. The drum and keys highways lacked that fallback — they only re-framed when the host explicitly called `resize(w, h)` — so their panels stayed framed for the pre-fullscreen size while the guitar/bass panels adapted (visible as a too-small, off-center highway after maximizing a split-screen session). Both draw loops now port `highway_3d`'s per-frame drift check: they re-apply on backing-store change (`canvas.width/height`) AND on CSS-box drift (`clientWidth/clientHeight` vs the last applied logical size, throttled to every 10th frame), and reset the tracking in `destroy()` so a reused instance re-frames on the next song. `plugins/drum_highway_3d` → 0.3.1, `plugins/keys_highway_3d` → 0.1.1. Tests: `tests/js/drum_keys_highway_3d_resize_reframe.test.js`.
|
||||
- **Tuner: finished the "remove unused settings" cleanup and fixed the sidebar panel position.** The Floating Button and Tuning Visibility settings sections were removed, but their config was still live: `disabledTunings` still filtered the tuner menu (with no UI left to re-enable a hidden tuning — a one-way trap) and `showFloatingButton` still gated the floating launcher. Both are now fully retired — the enforcement paths in `plugins/tuner/screen.js`/`utils/ui.js` and the persistence in `plugins/tuner/routes.py` are gone (and `routes.py` strips the retired keys on write, so stale values are purged). The tuner panel opened from the v3 sidebar Plugins rail popover now anchors beside it via the host's stable plugin-control slot API (falling back to the popover id), is **clamped to the viewport** so it can't open off the right/bottom edge on narrow/short windows, and re-anchors on window resize. `plugins/tuner` → 1.3.3.
|
||||
|
||||
### Added
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "drum_highway_3d",
|
||||
"name": "3D Drum Highway",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"type": "visualization",
|
||||
"bundled": true,
|
||||
"script": "screen.js",
|
||||
|
||||
@ -1390,6 +1390,16 @@
|
||||
// multiplied into the device pixel ratio like highway_3d does.
|
||||
let _renderScale = 1;
|
||||
|
||||
// Auto-resize fallback state (PORTED FROM highway_3d — keep in sync).
|
||||
// The splitscreen host resizes the panel canvas but overrides
|
||||
// hw.resize and never calls our resize(w,h), so draw() self-detects
|
||||
// size drift. _lastHwW/H track the backing store last seen; _appliedW/H
|
||||
// track the logical size last handed to applySize(); the countdown
|
||||
// throttles the per-frame clientWidth/Height read.
|
||||
let _lastHwW = 0, _lastHwH = 0;
|
||||
let _appliedW = 0, _appliedH = 0;
|
||||
let _boxCheckCountdown = 0;
|
||||
|
||||
// Bloom composer state (PORTED FROM highway_3d/screen.js — keep in sync).
|
||||
let _composer = null;
|
||||
let _bloomPass = null;
|
||||
@ -3106,6 +3116,10 @@
|
||||
}
|
||||
cam.aspect = W / H;
|
||||
cam.updateProjectionMatrix();
|
||||
// Record the logical size actually framed for, so the draw()
|
||||
// drift check can tell when the live canvas box has moved away
|
||||
// from it (PORTED FROM highway_3d).
|
||||
_appliedW = W; _appliedH = H;
|
||||
_sizeFxCanvas();
|
||||
}
|
||||
|
||||
@ -3372,6 +3386,33 @@
|
||||
_renderScale = newScale;
|
||||
applySize(highwayCanvas.clientWidth, highwayCanvas.clientHeight);
|
||||
}
|
||||
// Keep the render matched to the highway canvas's real box
|
||||
// (PORTED FROM highway_3d — keep in sync). Two drifts to catch:
|
||||
// 1. Backing store (canvas.width/height) changed out from under
|
||||
// us — the splitscreen hw.resize override resizes the element
|
||||
// but never calls renderer.resize().
|
||||
// 2. The CSS box (clientWidth/Height) drifted while the backing
|
||||
// store held — e.g. the flex #highway box settling after a
|
||||
// fullscreen transition, with no backing-store change and no
|
||||
// resize() call, so branch 1 never fires. Without this the
|
||||
// drum/keys panels stay framed for the pre-fullscreen size.
|
||||
if (highwayCanvas) {
|
||||
const _bsChanged = highwayCanvas.width !== _lastHwW
|
||||
|| highwayCanvas.height !== _lastHwH;
|
||||
_boxCheckCountdown = (_boxCheckCountdown + 1) % 10;
|
||||
if (_bsChanged || _boxCheckCountdown === 0) {
|
||||
const _bw = highwayCanvas.clientWidth | 0;
|
||||
const _bh = highwayCanvas.clientHeight | 0;
|
||||
if (_bsChanged) {
|
||||
_lastHwW = highwayCanvas.width;
|
||||
_lastHwH = highwayCanvas.height;
|
||||
if (_bw > 0 && _bh > 0) applySize(_bw, _bh);
|
||||
} else if (_bw > 0 && _bh > 0 &&
|
||||
(Math.abs(_bw - _appliedW) > 1 || Math.abs(_bh - _appliedH) > 1)) {
|
||||
applySize(_bw, _bh);
|
||||
}
|
||||
}
|
||||
}
|
||||
rebuildNotes(bundle);
|
||||
// Wall-clock FX step (sparks, kick pulse) — decoupled from
|
||||
// song time so effects keep settling while paused/seeking.
|
||||
@ -3447,6 +3488,11 @@
|
||||
}
|
||||
if (_instances.size === 0) _midiReleaseSession();
|
||||
teardown(); // includes _removeHud()
|
||||
// Instances are reused across songs (destroy() → init()); stale
|
||||
// applied/backing dims would suppress the first reframe of the
|
||||
// next song (PORTED FROM highway_3d).
|
||||
_lastHwW = 0; _lastHwH = 0;
|
||||
_appliedW = 0; _appliedH = 0;
|
||||
highwayCanvas = null;
|
||||
},
|
||||
// Exposed for module-level MIDI router. The receiver runs on
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "keys_highway_3d",
|
||||
"name": "Keys Highway 3D",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "RS+-style 3D falling-note piano highway fed by the Sloppak Notation Format, with Web MIDI input scoring.",
|
||||
"type": "visualization",
|
||||
"bundled": true,
|
||||
|
||||
@ -1338,6 +1338,15 @@
|
||||
// Host adaptive-quality scale (bundle.renderScale, 0.25–1) —
|
||||
// multiplied into the device pixel ratio like highway_3d does.
|
||||
let _renderScale = 1;
|
||||
// Auto-resize fallback state (PORTED FROM highway_3d — keep in sync).
|
||||
// The splitscreen host resizes the panel canvas but overrides
|
||||
// hw.resize and never calls our resize(w,h), so draw() self-detects
|
||||
// size drift. _lastHwW/H track the backing store last seen; _appliedW/H
|
||||
// track the logical size last handed to applySize(); the countdown
|
||||
// throttles the per-frame clientWidth/Height read.
|
||||
let _lastHwW = 0, _lastHwH = 0;
|
||||
let _appliedW = 0, _appliedH = 0;
|
||||
let _boxCheckCountdown = 0;
|
||||
// Bloom composer state (PORTED FROM highway_3d/screen.js — keep in sync).
|
||||
let _composer = null;
|
||||
let _bloomPass = null;
|
||||
@ -3131,6 +3140,10 @@
|
||||
}
|
||||
cam.aspect = w / h;
|
||||
cam.updateProjectionMatrix();
|
||||
// Record the logical size actually framed for, so the draw()
|
||||
// drift check can tell when the live canvas box has moved away
|
||||
// from it (PORTED FROM highway_3d).
|
||||
_appliedW = w; _appliedH = h;
|
||||
_sizeFxCanvas();
|
||||
}
|
||||
|
||||
@ -3302,6 +3315,33 @@
|
||||
_renderScale = newScale;
|
||||
applySize(highwayCanvas.clientWidth, highwayCanvas.clientHeight);
|
||||
}
|
||||
// Keep the render matched to the highway canvas's real box
|
||||
// (PORTED FROM highway_3d — keep in sync). Two drifts to catch:
|
||||
// 1. Backing store (canvas.width/height) changed out from under
|
||||
// us — the splitscreen hw.resize override resizes the element
|
||||
// but never calls renderer.resize().
|
||||
// 2. The CSS box (clientWidth/Height) drifted while the backing
|
||||
// store held — e.g. the flex #highway box settling after a
|
||||
// fullscreen transition, with no backing-store change and no
|
||||
// resize() call, so branch 1 never fires. Without this the
|
||||
// drum/keys panels stay framed for the pre-fullscreen size.
|
||||
if (highwayCanvas) {
|
||||
const _bsChanged = highwayCanvas.width !== _lastHwW
|
||||
|| highwayCanvas.height !== _lastHwH;
|
||||
_boxCheckCountdown = (_boxCheckCountdown + 1) % 10;
|
||||
if (_bsChanged || _boxCheckCountdown === 0) {
|
||||
const _bw = highwayCanvas.clientWidth | 0;
|
||||
const _bh = highwayCanvas.clientHeight | 0;
|
||||
if (_bsChanged) {
|
||||
_lastHwW = highwayCanvas.width;
|
||||
_lastHwH = highwayCanvas.height;
|
||||
if (_bw > 0 && _bh > 0) applySize(_bw, _bh);
|
||||
} else if (_bw > 0 && _bh > 0 &&
|
||||
(Math.abs(_bw - _appliedW) > 1 || Math.abs(_bh - _appliedH) > 1)) {
|
||||
applySize(_bw, _bh);
|
||||
}
|
||||
}
|
||||
}
|
||||
const now = (bundle && typeof bundle.currentTime === 'number') ? bundle.currentTime : 0;
|
||||
if (_notation) updateScene(now);
|
||||
_animateFeedback(performance.now());
|
||||
@ -3363,6 +3403,11 @@
|
||||
}
|
||||
if (_instances.size === 0) _midiReleaseSession();
|
||||
teardown(); // includes _removeHud()
|
||||
// Instances are reused across songs (destroy() → init()); stale
|
||||
// applied/backing dims would suppress the first reframe of the
|
||||
// next song (PORTED FROM highway_3d).
|
||||
_lastHwW = 0; _lastHwH = 0;
|
||||
_appliedW = 0; _appliedH = 0;
|
||||
highwayCanvas = null;
|
||||
},
|
||||
|
||||
|
||||
83
tests/js/drum_keys_highway_3d_resize_reframe.test.js
Normal file
83
tests/js/drum_keys_highway_3d_resize_reframe.test.js
Normal file
@ -0,0 +1,83 @@
|
||||
// Pins the auto-reframe-on-layout-drift behaviour ported into the drum and
|
||||
// keys 3D highway renderers from plugins/highway_3d/screen.js.
|
||||
//
|
||||
// Bug it guards against: under splitscreen the host resizes each panel's
|
||||
// canvas but overrides hw.resize and never calls renderer.resize(). The
|
||||
// guitar/bass highway_3d self-detects this in draw(); the drum and keys
|
||||
// highways originally only re-framed when the host called resize(w,h), so
|
||||
// their panels did NOT resize when the app went fullscreen — they stayed
|
||||
// framed for the pre-fullscreen size while the guitar/bass panels adapted.
|
||||
//
|
||||
// The fix ports highway_3d's per-frame drift check into both draw() loops:
|
||||
// compare the live canvas backing store (canvas.width/height) AND the CSS box
|
||||
// (clientWidth/Height) against the last applied logical size, re-running
|
||||
// applySize() on either drift. A refactor that drops this check, stops
|
||||
// recording _appliedW/_appliedH, or reverts to resize()-only sizing would
|
||||
// silently bring the fullscreen-split bug back.
|
||||
//
|
||||
// Source-level only — same strategy as highway_3d_resize_reframe.test.js.
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const PLUGINS = path.join(__dirname, '..', '..', 'plugins');
|
||||
const CASES = [
|
||||
{ name: 'drum_highway_3d', file: path.join(PLUGINS, 'drum_highway_3d', 'screen.js') },
|
||||
{ name: 'keys_highway_3d', file: path.join(PLUGINS, 'keys_highway_3d', 'screen.js') },
|
||||
];
|
||||
|
||||
for (const { name, file } of CASES) {
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
|
||||
test(`${name}: applied-size tracking is declared as instance state`, () => {
|
||||
assert.match(
|
||||
src,
|
||||
/let\s+_appliedW\s*=\s*0\s*,\s*_appliedH\s*=\s*0\s*;/,
|
||||
'_appliedW / _appliedH must be declared as per-instance state',
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/let\s+_lastHwW\s*=\s*0\s*,\s*_lastHwH\s*=\s*0\s*;/,
|
||||
'_lastHwW / _lastHwH must be declared as per-instance state',
|
||||
);
|
||||
});
|
||||
|
||||
test(`${name}: applySize records the logical w/h it applied`, () => {
|
||||
assert.match(
|
||||
src,
|
||||
/_appliedW\s*=\s*[wW]\s*;\s*_appliedH\s*=\s*[hH]\s*;/,
|
||||
'applySize must record _appliedW / _appliedH',
|
||||
);
|
||||
});
|
||||
|
||||
test(`${name}: draw() preserves the backing-store drift branch (splitscreen path)`, () => {
|
||||
assert.match(
|
||||
src,
|
||||
/const\s+_bsChanged\s*=\s*highwayCanvas\.width\s*!==\s*_lastHwW\s*\|\|\s*highwayCanvas\.height\s*!==\s*_lastHwH\s*;/,
|
||||
'the backing-store (canvas.width/height) comparison must run every frame',
|
||||
);
|
||||
assert.match(
|
||||
src,
|
||||
/if\s*\(\s*_bsChanged\s*\)\s*\{\s*_lastHwW\s*=\s*highwayCanvas\.width\s*;\s*_lastHwH\s*=\s*highwayCanvas\.height\s*;[\s\S]*?applySize\(\s*_bw\s*,\s*_bh\s*\)\s*;/,
|
||||
'the backing-store drift branch must re-apply',
|
||||
);
|
||||
});
|
||||
|
||||
test(`${name}: draw() re-applies on CSS-box drift without a backing-store change`, () => {
|
||||
assert.match(
|
||||
src,
|
||||
/else if\s*\(\s*_bw\s*>\s*0\s*&&\s*_bh\s*>\s*0\s*&&\s*\(\s*Math\.abs\(\s*_bw\s*-\s*_appliedW\s*\)\s*>\s*1\s*\|\|\s*Math\.abs\(\s*_bh\s*-\s*_appliedH\s*\)\s*>\s*1\s*\)\s*\)\s*\{\s*applySize\(\s*_bw\s*,\s*_bh\s*\)\s*;/,
|
||||
'draw() must re-apply when the live box drifts >1px from _appliedW/_appliedH',
|
||||
);
|
||||
});
|
||||
|
||||
test(`${name}: destroy() resets the applied-size tracking`, () => {
|
||||
assert.match(
|
||||
src,
|
||||
/_lastHwW\s*=\s*0\s*;\s*_lastHwH\s*=\s*0\s*;\s*_appliedW\s*=\s*0\s*;\s*_appliedH\s*=\s*0\s*;/,
|
||||
'destroy() must reset the drift-tracking state to 0',
|
||||
);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user