0.2.3: add independent browser evidence for held poses
This commit is contained in:
commit
7e1db340e8
2 files changed
+100
No files matched your search
@@ -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
|
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.
|
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
|
### 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.
|
Ported from [PR #427](https://github.com/sganggs/Stronghold-Protocol/pull/427) by @YinqiMargatroid.
|
||||||
|
|||||||
@@ -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(); }
|
||||||
|
});
|
||||||
Reference in new issue
Block a user