Verify held Spine poses and delayed font textures in real Chrome (#434)

This commit is contained in:
sganggs committed 2026-10-09 17:38:12 +08:00
1 parent e4e28117bb
commit 6036e6a4ba
2 files changed
+100

No files matched your search

+5
View File
@@ -131,6 +131,11 @@ Redeploy countdown labels share rasterized textures keyed by text, colour and re
them in place. The 64 targeted renderer tests passed. The author's performance measurements are not reproduced
here; real Pixi checks are part of the integration pass. No battle rule or golden result is intentionally changed.
Real Chrome verification additionally renders one actual Spine death against continual forced updates through
mixing and the held final frame (40 pixel comparisons over 600 frames), checks revival and the actor clock under the held-update policy,
and reloads Bender after a shared label was rasterised. The label updates in place and DPR variants stay separate.
This verifies correctness on that model; it is not a new performance benchmark or a full-roster pixel claim.
### 28.9 Preserve facing during pushes and pulls — #418 / PR #427
Ported from [PR #427](https://github.com/sganggs/Stronghold-Protocol/pull/427) by @YinqiMargatroid.
+95
View File
@@ -0,0 +1,95 @@
// PR #434: real pixi-spine's final pose/mixing and shared ring textures, beyond the fake-PIXI unit tests.
// RENDER_E2E=1 node --test test/render/held-pose.browser.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { existsSync } from 'node:fs';
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const skip = process.env.RENDER_E2E !== '1' || !existsSync(CHROME);
test('real Spine held death matches forced updates; revival and late font rasterisation remain live', { skip, timeout: 90000 }, async () => {
const { startServer } = await import('../../server/index.js');
const puppeteer = (await import('puppeteer-core')).default;
const srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
let browser;
try {
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-first-run'] });
const page = await browser.newPage();
const errors = [];
page.on('pageerror', e => errors.push(e.message));
page.on('console', m => { if (m.type() === 'error') console.error(m.text()); });
await page.goto(`http://127.0.0.1:${srv.port}/dev/render-demo.html?scene=normal-m01&t=18&paused=1&panel=0`);
await page.waitForFunction(() => window.__demo?.ready && [...window.__demo.view.debug.views.values()].some(v => v.actor?.dieClip()), { timeout: 30000 });
const result = await page.evaluate(async () => {
const { SpineActor } = await import('/js/render/spine.js');
const T = await import('/js/render/textures.js');
const { app, views } = window.__demo.view.debug;
app.stop();
const source = [...views.values()].find(v => v.actor?.dieClip()).actor;
const a = new SpineActor(source.spine.spineData, source.entry);
const b = new SpineActor(source.spine.spineData, source.entry);
const renderer = app.renderer;
const hash = obj => {
const pixels = renderer.extract.pixels(obj);
let value = 2166136261, ink = 0;
for (let i = 0; i < pixels.length; i++) { value = Math.imul(value ^ pixels[i], 16777619); if (i % 4 === 3 && pixels[i]) ink++; }
return { value: value >>> 0, bytes: pixels.length, ink };
};
a.update(0.1);b.update(0.1);a.die();b.die();
const firstHeld = a.poseHeld();
// Exercise a real mix that outlasts the outgoing death clip, not merely the unposed first frame.
for (const actor of [a, b]) {
const track = actor.spine.state.tracks[0];
track.mixDuration = track.animationEnd - track.animationStart + 0.25;
}
let skipped = 0, equal = true, ink = 0, endMixFrames = 0, mixedHeld = false;
for (let i = 0; i < 600; i++) {
const track = a.spine.state.tracks[0];
if (track.mixingFrom && track.trackTime >= track.animationEnd - track.animationStart) {
endMixFrames++;mixedHeld ||= a.poseHeld();
}
if (a.poseHeld()) { a.clock += 1 / 60;skipped++; } else a.update(1 / 60);
b.update(1 / 60);
if (i % 15 === 0) {
const x = hash(a.spine), y = hash(b.spine);
equal &&= x.value === y.value && x.bytes === y.bytes;
ink = Math.max(ink, x.ink);
}
}
const finalA = hash(a.spine), finalB = hash(b.spine);
const finalVisibleEqual = a.poseHeld() && finalA.ink > 0 && finalA.value === finalB.value && finalA.bytes === finalB.bytes;
const clocksEqual = Math.abs(a.clock - b.clock) < 1e-9;
a.revive();a.deploy();a.update(1 / 60);
const revived = !a.poseHeld() && !a.dead;
a.spine.destroy({ children: true });b.spine.destroy({ children: true });
// A label created before Bender loads must keep its texture identity when real glyphs arrive.
await document.fonts.ready;
document.querySelector('link[href="/fonts/fonts.css"]').remove();
await Promise.resolve();
const benderAbsent = ![...document.fonts].some(f => f.family.replace(/["']/g, '') === 'Bender');
const label = T.downLabel('470', '#ffffff', 1);
const sprite = new PIXI.Sprite(label), before = hash(sprite);
const font = new FontFace('Bender', 'url(/fonts/bender-regular.woff2)', { weight: '400' });
await font.load();document.fonts.add(font);T.refreshDownLabels();
const after = hash(sprite);
const sameTexture = T.downLabel('470', '#ffffff', 1) === label;
const separateResolution = T.downLabel('470', '#ffffff', 2) !== label;
sprite.destroy();
return { firstHeld, skipped, equal, ink, finalVisibleEqual, clocksEqual, revived, sameTexture, separateResolution, endMixFrames, mixedHeld,
benderAbsent, fontChanged: before.value !== after.value || before.bytes !== after.bytes, fontInk: after.ink };
});
assert.equal(result.firstHeld, false, 'initial pose / mix must run');
assert.ok(result.skipped > 100, JSON.stringify(result));
assert.ok(result.endMixFrames > 0, 'real runtime mix remains after the death clip ends');
assert.equal(result.mixedHeld, false, 'mixed final poses still advance');
assert.equal(result.equal, true, 'held final frame is pixel-identical to continual updates');
assert.ok(result.ink > 0, 'the comparison renders actual art');
assert.equal(result.finalVisibleEqual, true, 'the held final pose itself is visible and identical');
assert.equal(result.clocksEqual, true);
assert.equal(result.revived, true);
assert.equal(result.sameTexture, true);
assert.equal(result.separateResolution, true);
assert.equal(result.benderAbsent, true, 'CSS font faces are absent before fallback rasterisation');
assert.equal(result.fontChanged, true, 'late Bender glyphs replace the fallback');
assert.ok(result.fontInk > 0);
assert.deepEqual(errors, []);
} catch (error) { console.error(error);throw error; } finally { await browser?.close();await srv.close(); }
});