perf(highway_3d): pre-warm shaders and label textures at init

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>
This commit is contained in:
OmikronApex
2026-07-01 23:53:36 +02:00
co-authored by Claude Fable 5
parent fd840011d2
commit 5239665e2b
2 changed files with 60 additions and 1 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "highway_3d",
"name": "3D Highway",
"version": "3.30.2",
"version": "3.31.0",
"type": "visualization",
"bundled": true,
"script": "screen.js",
+59
View File
@@ -9886,6 +9886,55 @@
}
/* ── Per-frame rendering ─────────────────────────────────────────── */
// ── GPU pre-warm (perf: first-appearance hitches) ─────────────────
// Three.js compiles a material's shader program and uploads a
// texture the first frame the owning object renders — profiled as
// mid-song frame spikes (getParameters / texSubImage2D). Pay those
// costs during init (load spinner) instead:
// _prewarmStatic() — ren.compile() over the fully-built scene
// + deterministic label textures (fret
// numbers in every per-frame style/colour
// combo).
// _prewarmChart(bundle) — chart-dependent labels (chord template
// names, section names); needs the ready
// bundle, so it runs once from the first
// draw() after each init.
// txtMat() rasterises into the unbounded cache these draws hit
// anyway; ren.initTexture() forces the GPU upload now.
let _chartPrewarmed = false;
function _prewarmTex(mat) {
if (mat && mat.map && ren) ren.initTexture(mat.map);
}
function _prewarmStatic() {
try {
if (ren && scene && cam) ren.compile(scene, cam);
} catch (e) { console.warn('[3D-Hwy] prewarm compile:', e); }
try {
for (let f = 0; f <= NFRETS; f++) {
_prewarmTex(txtMat(f, FRET_LABEL_GOLD_HEX, false, 'noteFret'));
_prewarmTex(txtMat(f, FRET_LABEL_GOLD_HEX, false, 'fretRow'));
_prewarmTex(txtMat(f, FRET_LABEL_IDLE_HEX, false, 'fretRow'));
_prewarmTex(txtMat(f, '#ffffff', false, 'ghostFret'));
}
} catch (e) { console.warn('[3D-Hwy] prewarm labels:', e); }
}
function _prewarmChart(bundle) {
try {
const tpls = bundle && bundle.chordTemplates;
if (Array.isArray(tpls)) {
for (const tpl of tpls) {
if (tpl && tpl.name) _prewarmTex(txtMat(tpl.name, '#e8d080', true, 'chord'));
}
}
const secs = bundle && bundle.sections;
if (Array.isArray(secs)) {
for (const s of secs) {
if (s && s.name) _prewarmTex(txtMat(s.name, '#00cccc', true, 'section'));
}
}
} catch (e) { console.warn('[3D-Hwy] prewarm chart labels:', e); }
}
function update(bundle) {
pbBeg(0);
// [verdict glow] Apply the level-driven verdict brightness captured
@@ -14962,6 +15011,12 @@
_invertedForBoard = _invertedCached;
_leftyForBoard = _leftyCached;
if (!initScene()) { _unsubscribeFocus(); _rejectReady(new Error('initScene failed')); return; }
// Pre-compile shaders + upload deterministic label
// textures while the load spinner is still up; the
// chart-dependent half runs on first draw() (bundle
// arrays are only guaranteed populated post-ready).
_prewarmStatic();
_chartPrewarmed = false;
const sz = canvasSize(highwayCanvas);
// Mark ready before RAF so any resize(w,h) calls that arrive
// in the meantime (e.g. from sizeCanvases()) are applied directly.
@@ -15000,6 +15055,10 @@
draw(bundle) {
if (!_isReady) return;
if (!_chartPrewarmed) {
_chartPrewarmed = true;
_prewarmChart(bundle);
}
_invertedCached = !!bundle.inverted;
_leftyCached = !!bundle.lefty;
const newNStr = resolveStringCount(bundle);