mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-12 03:41:40 +00:00
fix(highway): the paused-frame throttle was throttling the whole venue
Pausing the song dropped the venue, the crowd and the stage to ~10 fps — "everything around the highway drops fps by a lot". draw() caps paused frames to one per _PAUSED_FRAME_INTERVAL_MS (100ms), on an assumption stated plainly in highway-constants.js: a heavy WebGL renderer "does a full render every frame even while paused. That is pure waste." That was true when a paused chart was a still picture. The venue broke the assumption. Its video backdrop keeps playing and its crowd reacts on a clock of their own, and BOTH are drawn into the same canvas as the notes — so a throttle aimed at static notes throttled the entire room. The scene only got a texture upload 10 times a second while the transport sat paused. Renderers can now declare that their picture is not static while the chart clock is stopped: an optional needsContinuousFrames(). The throttle is skipped only when it returns exactly true, and the probe fails closed — a renderer that doesn't implement it, or one that throws, keeps the throttle unchanged. So the GPU saving that motivated #654 survives everywhere it was actually valid. highway_3d implements it and claims continuous frames ONLY while a crowd video is genuinely rolling (bound, unpaused, not ended, readyState >= 2). With no venue pack — the common case — the paused scene really is static, so it keeps the throttle and the GPU still idles. Tests extend tests/js/highway_pause_throttle.test.js, which guards this code path source-level (the draw loop owns the rAF + WebGL lifecycle and is deliberately not reproduced in a vm — see the file header). The new guards pin that the capability GATES the early return rather than merely being called near it, that the probe fails closed on absent/non-function/throwing/truthy-but-not- true, and that the 3D renderer keys off the real video elements and can still return false. All 3 fail against the pre-fix source. eslint 0 errors; JS 1202/1202; pytest 2597 passed.
This commit is contained in:
@@ -15388,6 +15388,27 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The host throttles paused frames to ~10 fps, on the assumption
|
||||||
|
// that a paused chart is a static picture and re-rendering it is
|
||||||
|
// pure waste (highway-constants._PAUSED_FRAME_INTERVAL_MS).
|
||||||
|
//
|
||||||
|
// That stopped being true when the venue landed. The venue backdrop
|
||||||
|
// is a PLAYING VIDEO and the crowd reacts on its own clock, and they
|
||||||
|
// are drawn into this same canvas as the highway — so throttling the
|
||||||
|
// highway throttled the whole room. Pausing the song dropped the
|
||||||
|
// venue, the crowd and the stage to 10 fps.
|
||||||
|
//
|
||||||
|
// Only claim continuous frames while a crowd video is actually
|
||||||
|
// rolling: with no venue pack (the common case) the paused scene IS
|
||||||
|
// static and the throttle should still save the GPU.
|
||||||
|
needsContinuousFrames() {
|
||||||
|
if (!_isReady || _ctxLost) return false;
|
||||||
|
for (const v of _venueCrowdVideos) {
|
||||||
|
if (v && !v.paused && !v.ended && v.readyState >= 2) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
draw(bundle) {
|
draw(bundle) {
|
||||||
if (!_isReady) return;
|
if (!_isReady) return;
|
||||||
if (_ctxLost) return; // GPU context lost (alt-tab / reset) — skip until restored
|
if (_ctxLost) return; // GPU context lost (alt-tab / reset) — skip until restored
|
||||||
|
|||||||
+20
-1
@@ -1159,6 +1159,17 @@ function createHighway() {
|
|||||||
' (user ' + hwState._renderScale.toFixed(2) + ' / auto ' + hwState._autoScale.toFixed(2) + ')';
|
' (user ' + hwState._renderScale.toFixed(2) + ' / auto ' + hwState._autoScale.toFixed(2) + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional renderer capability: "my picture keeps moving even when the chart
|
||||||
|
// clock is stopped". Anything a renderer animates on its own clock (the 3D
|
||||||
|
// highway's venue video + crowd) has to opt out of the paused-frame throttle
|
||||||
|
// or it renders at 10 fps while the song is paused. Absent / throwing =
|
||||||
|
// false, so every existing renderer keeps the throttle unchanged.
|
||||||
|
function _rendererNeedsContinuousFrames() {
|
||||||
|
const r = hwState._renderer;
|
||||||
|
if (!r || typeof r.needsContinuousFrames !== 'function') return false;
|
||||||
|
try { return r.needsContinuousFrames() === true; } catch (_) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
hwState.animFrame = requestAnimationFrame(draw);
|
hwState.animFrame = requestAnimationFrame(draw);
|
||||||
if (!hwState.canvas || !hwState._renderer) return;
|
if (!hwState.canvas || !hwState._renderer) return;
|
||||||
@@ -1223,7 +1234,15 @@ function createHighway() {
|
|||||||
const _nowP = performance.now();
|
const _nowP = performance.now();
|
||||||
if (_nowP - hwState._chartLastAdvanceAt > _CHART_MAX_INTERP_MS) {
|
if (_nowP - hwState._chartLastAdvanceAt > _CHART_MAX_INTERP_MS) {
|
||||||
_paused = true;
|
_paused = true;
|
||||||
if (_nowP - hwState._lastPausedDrawAt < _PAUSED_FRAME_INTERVAL_MS) return;
|
// ...unless the renderer says its picture is NOT static while
|
||||||
|
// paused. The throttle assumes a paused chart is a still frame,
|
||||||
|
// but a renderer can own content on a clock of its own — the 3D
|
||||||
|
// highway draws the venue's video backdrop and its reactive crowd
|
||||||
|
// into this same canvas, so throttling the highway throttled the
|
||||||
|
// whole room to 10 fps whenever the song was paused. Optional
|
||||||
|
// method: renderers that don't implement it keep the throttle.
|
||||||
|
if (!_rendererNeedsContinuousFrames()
|
||||||
|
&& _nowP - hwState._lastPausedDrawAt < _PAUSED_FRAME_INTERVAL_MS) return;
|
||||||
hwState._lastPausedDrawAt = _nowP;
|
hwState._lastPausedDrawAt = _nowP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,3 +77,50 @@ test('throttle runs after the ready gate, before bundle/draw', () => {
|
|||||||
assert.ok(readyIdx < throttleIdx, 'throttle must come after the ready gate');
|
assert.ok(readyIdx < throttleIdx, 'throttle must come after the ready gate');
|
||||||
assert.ok(throttleIdx < drawIdx, 'throttle must come before the renderer draw');
|
assert.ok(throttleIdx < drawIdx, 'throttle must come before the renderer draw');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── The throttle must not starve a renderer that animates on its own clock ──
|
||||||
|
//
|
||||||
|
// The throttle assumes a paused chart is a still picture, so re-rendering it is
|
||||||
|
// waste. That stopped being true when the venue landed: the 3D highway draws the
|
||||||
|
// venue's VIDEO backdrop and its reactive crowd into the same canvas as the
|
||||||
|
// notes, so capping paused frames capped the whole room — pausing the song
|
||||||
|
// dropped the venue to ~10 fps ("everything around the highway drops fps").
|
||||||
|
//
|
||||||
|
// Renderers now opt out via an optional needsContinuousFrames(). Absent or
|
||||||
|
// throwing must mean false, so every other renderer keeps the throttle.
|
||||||
|
|
||||||
|
test('paused throttle defers to a renderer that needs continuous frames', () => {
|
||||||
|
const src = highwaySources();
|
||||||
|
const fn = extractBlock(src, 'function draw()');
|
||||||
|
assert.match(fn, /_rendererNeedsContinuousFrames\s*\(\s*\)/,
|
||||||
|
'the paused throttle must consult the renderer capability');
|
||||||
|
// The capability must GATE the early-return, not merely be called near it:
|
||||||
|
// the throttle only applies when the renderer does NOT need every frame.
|
||||||
|
assert.match(
|
||||||
|
fn,
|
||||||
|
/!\s*_rendererNeedsContinuousFrames\s*\(\s*\)[\s\S]{0,160}_PAUSED_FRAME_INTERVAL_MS[\s\S]{0,40}return;/,
|
||||||
|
'throttle must be skipped when the renderer needs continuous frames',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the capability probe fails closed (absent / non-function / throwing)', () => {
|
||||||
|
const src = highwaySources();
|
||||||
|
const fn = extractBlock(src, 'function _rendererNeedsContinuousFrames()');
|
||||||
|
assert.match(fn, /typeof\s+r\.needsContinuousFrames\s*!==\s*'function'[\s\S]{0,40}return false/,
|
||||||
|
'a renderer without the method must keep the throttle');
|
||||||
|
assert.match(fn, /catch[\s\S]{0,40}return false/,
|
||||||
|
'a throwing renderer must keep the throttle, not crash the draw loop');
|
||||||
|
assert.match(fn, /===\s*true/,
|
||||||
|
'only an explicit true opts out — a truthy accident must not disable the throttle');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('3D highway claims continuous frames only while a crowd video is rolling', () => {
|
||||||
|
const h3d = fs.readFileSync(
|
||||||
|
path.join(__dirname, '..', '..', 'plugins', 'highway_3d', 'screen.js'), 'utf8');
|
||||||
|
const fn = extractBlock(h3d, 'needsContinuousFrames()');
|
||||||
|
assert.match(fn, /_venueCrowdVideos/, 'must key off the actual crowd video elements');
|
||||||
|
assert.match(fn, /\.paused/, 'a paused video is a still frame — throttle should still apply');
|
||||||
|
// With no venue pack (the common case) the paused scene really is static and
|
||||||
|
// the GPU saving must survive: the method has to be able to return false.
|
||||||
|
assert.match(fn, /return false;/, 'must fall through to false with no live video');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user