mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge branch 'main' into feat/voice-geometry
# Conflicts: # SECURITY.md # scripts/format-scope.json # src/app/constructCatalog.test.mjs # src/app/sourceComposition.test.mjs # src/firstRunExperience.test.mjs # src/i18n/locales/ar.json # src/i18n/locales/de.json # src/i18n/locales/es.json # src/i18n/locales/fr.json # src/i18n/locales/it.json # src/i18n/locales/ja.json # src/i18n/locales/pt.json # src/i18n/locales/ru.json # src/i18n/locales/uk.json # src/i18n/locales/zh-CN.json # src/i18n/locales/zh-Hant.json # src/voice/actionSchemas.test.mjs
This commit is contained in:
commit
51637a5e96
56 files changed
+1834
-208
No files matched your search
@@ -1,11 +1,27 @@
|
||||
/** A reveal must beat the one-second idle rotation refresh, regardless of FPS. */
|
||||
/**
|
||||
* Wall-clock budget for EACH observed frame, regardless of FPS. Both the settle
|
||||
* frame (measured from the forced moveEnd) and the reveal frame (measured from
|
||||
* the settle frame that hid the contact) must land inside it, so neither can be
|
||||
* repaired by the one-second idle rotation refresh before it is read. The
|
||||
* refresh clock starts at the fleet tick (preRender) of the frame the budget is
|
||||
* measured from, so the budget must leave room for that frame's own render
|
||||
* time: half the refresh interval tolerates frames up to ~500 ms, and a ~300 ms
|
||||
* software-rendered frame (headless SwiftShader) fits with margin.
|
||||
*/
|
||||
export const ARRIVAL_OBSERVATION_MS = 500;
|
||||
|
||||
/** A measured rotation error at or above this fails the arrival check. */
|
||||
export const ARRIVAL_MAX_ERROR_DEG = 10;
|
||||
|
||||
/** Attempts allowed when every earlier one timed out without a wrong angle. */
|
||||
export const ARRIVAL_MAX_ATTEMPTS = 2;
|
||||
|
||||
/**
|
||||
* Observe the first completed moveEnd frame and the first subsequent reveal.
|
||||
* The deadline starts when moveEnd is forced, before its rotation baseline, so
|
||||
* waiting for either frame cannot let the one-second idle refresh mask a bug.
|
||||
* `read` snapshots visibility and rotation error against the current position.
|
||||
* The settle deadline starts when moveEnd is forced, before its rotation
|
||||
* baseline; the reveal deadline restarts at the settle frame, which is where
|
||||
* the contact is hidden. `read` snapshots visibility and rotation error
|
||||
* against the current position.
|
||||
*/
|
||||
export function observeArrivalRotation({
|
||||
scene,
|
||||
@@ -18,6 +34,7 @@ export function observeArrivalRotation({
|
||||
}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startedAt = now();
|
||||
let phaseStartedAt = startedAt;
|
||||
let moveEnd = null;
|
||||
let moveEndMs = null;
|
||||
let revealFrames = 0;
|
||||
@@ -34,24 +51,42 @@ export function observeArrivalRotation({
|
||||
cleanup();
|
||||
resolve(result);
|
||||
};
|
||||
const expired = () => ({
|
||||
// A timeout keeps every angle it measured: the settle angle before a
|
||||
// reveal timeout, and the angle on a late frame itself (a late frame is
|
||||
// read first; the idle refresh can only repair an angle, so a wrong one
|
||||
// seen late was wrong on time too). `arrivalAngleViolated` then makes the
|
||||
// timeout final instead of retryable.
|
||||
const expired = (late = null) => ({
|
||||
error: `arrival ${moveEnd ? 'reveal' : 'moveEnd'} observation exceeded ${ARRIVAL_OBSERVATION_MS} ms`,
|
||||
elapsedMs: now() - startedAt,
|
||||
moveEndMs,
|
||||
...(moveEnd
|
||||
? { dMoveEnd: moveEnd.rotationError }
|
||||
: late
|
||||
? { dMoveEnd: late.rotationError }
|
||||
: {}),
|
||||
...(moveEnd && late?.shown ? { dReveal: late.rotationError } : {}),
|
||||
flipFrame: -1,
|
||||
});
|
||||
const armDeadline = () => {
|
||||
clearTimer(timer);
|
||||
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
|
||||
};
|
||||
remove = scene.postRender.addEventListener(() => {
|
||||
if (done) return;
|
||||
try {
|
||||
const elapsedMs = now() - startedAt;
|
||||
if (elapsedMs >= ARRIVAL_OBSERVATION_MS) {
|
||||
finish(expired());
|
||||
const at = now();
|
||||
const elapsedMs = at - startedAt;
|
||||
const sample = read();
|
||||
if (at - phaseStartedAt >= ARRIVAL_OBSERVATION_MS) {
|
||||
finish(expired(sample));
|
||||
return;
|
||||
}
|
||||
const sample = read();
|
||||
if (moveEnd === null) {
|
||||
moveEnd = sample;
|
||||
moveEndMs = elapsedMs;
|
||||
phaseStartedAt = at;
|
||||
armDeadline();
|
||||
startReveal();
|
||||
} else {
|
||||
revealFrames++;
|
||||
@@ -76,7 +111,7 @@ export function observeArrivalRotation({
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
timer = setTimer(() => finish(expired()), ARRIVAL_OBSERVATION_MS);
|
||||
armDeadline();
|
||||
try {
|
||||
startMoveEnd();
|
||||
scene.requestRender();
|
||||
@@ -87,14 +122,51 @@ export function observeArrivalRotation({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an observation measured a rotation error over the threshold on any
|
||||
* frame it captured, including the settle frame of a reveal that then timed
|
||||
* out. Such an observation is a demonstrated failure, never inconclusive.
|
||||
*/
|
||||
export function arrivalAngleViolated(observation) {
|
||||
const limit = (ARRIVAL_MAX_ERROR_DEG * Math.PI) / 180;
|
||||
return [observation?.dMoveEnd, observation?.dReveal].some(
|
||||
(error) => Number.isFinite(error) && error >= limit,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe arrival rotation, retrying only an inconclusive timeout. A wall-clock
|
||||
* timeout means the frame landed where the idle refresh could already have
|
||||
* repaired it, so it earns another attempt; an observation that measured a
|
||||
* wrong angle (or succeeded) is final, so a later clean attempt can never
|
||||
* replace it. `prepare` runs before every attempt; the result carries
|
||||
* `attempts`.
|
||||
*/
|
||||
export async function observeArrivalRotationWithRetry({
|
||||
prepare = async () => {},
|
||||
observe = observeArrivalRotation,
|
||||
maxAttempts = ARRIVAL_MAX_ATTEMPTS,
|
||||
...options
|
||||
}) {
|
||||
let observed;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
await prepare();
|
||||
observed = { ...(await observe(options)), attempts: attempt };
|
||||
if (!observed.error || arrivalAngleViolated(observed)) break;
|
||||
}
|
||||
return observed;
|
||||
}
|
||||
|
||||
/** Test the captured frames without retrying a bad first observation. */
|
||||
export function arrivalRotationPassed(observation) {
|
||||
return (
|
||||
!!observation &&
|
||||
!observation.error &&
|
||||
observation.shown === true &&
|
||||
observation.elapsedMs < ARRIVAL_OBSERVATION_MS &&
|
||||
(observation.dMoveEnd * 180) / Math.PI < 10 &&
|
||||
(observation.dReveal * 180) / Math.PI < 10
|
||||
observation.moveEndMs < ARRIVAL_OBSERVATION_MS &&
|
||||
observation.revealMs < ARRIVAL_OBSERVATION_MS &&
|
||||
Number.isFinite(observation.dMoveEnd) &&
|
||||
Number.isFinite(observation.dReveal) &&
|
||||
!arrivalAngleViolated(observation)
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,23 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import * as arrivalModule from './trackArrivalObservation.mjs';
|
||||
import {
|
||||
ARRIVAL_OBSERVATION_MS,
|
||||
arrivalAngleViolated,
|
||||
arrivalRotationPassed,
|
||||
observeArrivalRotation,
|
||||
observeArrivalRotationWithRetry,
|
||||
} from './trackArrivalObservation.mjs';
|
||||
|
||||
function fixture() {
|
||||
function fixture(start = observeArrivalRotation) {
|
||||
let time = 0;
|
||||
let state = { shown: true, rotationError: 0 };
|
||||
const listeners = new Set();
|
||||
const timers = new Map();
|
||||
let renders = 0;
|
||||
let moveEnds = 0;
|
||||
const observation = observeArrivalRotation({
|
||||
const observation = start({
|
||||
scene: {
|
||||
postRender: {
|
||||
addEventListener(listener) {
|
||||
@@ -110,7 +114,8 @@ test('no reveal fails at a deadline before the one-second idle catch-up', async
|
||||
assert.equal(ARRIVAL_OBSERVATION_MS, 500);
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
f.frame(480);
|
||||
f.expire(500);
|
||||
// The reveal budget is measured from the 8 ms settle frame.
|
||||
f.expire(508);
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /reveal.*500 ms/);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
@@ -123,20 +128,46 @@ test('no reveal fails at a deadline before the one-second idle catch-up', async
|
||||
test('a late visible frame fails even if the timeout callback is delayed', async () => {
|
||||
const f = fixture();
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
f.frame(501, { shown: true, rotationError: 0 });
|
||||
f.frame(509, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /reveal.*500 ms/);
|
||||
assert.equal(result.elapsedMs, 501);
|
||||
assert.equal(result.elapsedMs, 509);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('the deadline includes moveEnd setup rather than restarting when hidden', async () => {
|
||||
test('a slow software renderer passes when each frame stays inside its own budget', async () => {
|
||||
// Headless SwiftShader frames run ~300 ms: settle lands at 300 ms and the
|
||||
// reveal at 600 ms from moveEnd. Both stay inside the 1 s idle refresh.
|
||||
const f = fixture();
|
||||
f.frame(400, { shown: true, rotationError: 0 });
|
||||
f.frame(300, { shown: true, rotationError: 0 });
|
||||
f.expire(500); // the settle frame re-armed the deadline to 800 ms
|
||||
f.frame(600, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.equal(result.moveEndMs, 400);
|
||||
assert.equal(result.moveEndMs, 300);
|
||||
assert.equal(result.revealMs, 300);
|
||||
assert.equal(result.elapsedMs, 600);
|
||||
assert.equal(result.flipFrame, 1);
|
||||
assert.equal(arrivalRotationPassed(result), true);
|
||||
assert.equal(f.resources.timers, 0);
|
||||
});
|
||||
|
||||
test('a settle frame later than its budget fails as a moveEnd timeout', async () => {
|
||||
const f = fixture();
|
||||
f.frame(500, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /moveEnd.*500 ms/);
|
||||
assert.equal(result.moveEndMs, null);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('a reveal later than its own budget fails even after a fast settle frame', async () => {
|
||||
const f = fixture();
|
||||
f.frame(400, { shown: true, rotationError: 0 });
|
||||
f.frame(901, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /reveal.*500 ms/);
|
||||
assert.equal(result.moveEndMs, 400);
|
||||
assert.equal(result.elapsedMs, 901);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
@@ -149,3 +180,195 @@ test('missing moveEnd frames fail without waiting for a render callback', async
|
||||
assert.equal(f.resources.listeners, 0);
|
||||
assert.equal(f.resources.timers, 0);
|
||||
});
|
||||
|
||||
// Let a retry's next attempt register its postRender listener.
|
||||
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
function retryFixture(extra = {}) {
|
||||
const counts = { prepares: 0 };
|
||||
const f = fixture((options) =>
|
||||
observeArrivalRotationWithRetry({
|
||||
...options,
|
||||
...extra,
|
||||
async prepare() {
|
||||
counts.prepares++;
|
||||
},
|
||||
}),
|
||||
);
|
||||
f.counts = counts;
|
||||
return f;
|
||||
}
|
||||
|
||||
test('a wrong settle angle followed by a reveal timeout is final, not retried into a pass', async () => {
|
||||
const f = retryFixture();
|
||||
await settle();
|
||||
// 0.5 rad ≈ 29°: a real threshold violation on the settle frame.
|
||||
f.frame(8, { shown: true, rotationError: 0.5 });
|
||||
f.expire(508); // the reveal never comes inside its budget
|
||||
await settle();
|
||||
// An otherwise clean second attempt, which must never be started.
|
||||
f.frame(516, { shown: true, rotationError: 0 });
|
||||
f.frame(524, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /reveal.*500 ms/);
|
||||
assert.equal(result.dMoveEnd, 0.5, 'the measured settle angle is kept');
|
||||
assert.equal(arrivalAngleViolated(result), true);
|
||||
assert.equal(result.attempts, 1);
|
||||
assert.equal(f.counts.prepares, 1);
|
||||
assert.equal(f.resources.moveEnds, 1);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
assert.equal(f.resources.listeners, 0);
|
||||
assert.equal(f.resources.timers, 0);
|
||||
});
|
||||
|
||||
test('an inconclusive reveal timeout after a correct settle earns one more attempt', async () => {
|
||||
const f = retryFixture();
|
||||
await settle();
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
f.expire(508);
|
||||
await settle();
|
||||
f.frame(516, { shown: true, rotationError: 0 });
|
||||
f.frame(596, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.equal(result.attempts, 2);
|
||||
assert.equal(f.counts.prepares, 2);
|
||||
assert.equal(f.resources.moveEnds, 2);
|
||||
assert.equal(arrivalRotationPassed(result), true);
|
||||
});
|
||||
|
||||
test('timeouts stop after the attempt limit and still fail', async () => {
|
||||
const f = retryFixture();
|
||||
await settle();
|
||||
f.expire(500);
|
||||
await settle();
|
||||
f.expire(1000);
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /moveEnd.*500 ms/);
|
||||
assert.equal(result.attempts, 2);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
assert.equal(f.resources.listeners, 0);
|
||||
assert.equal(f.resources.timers, 0);
|
||||
});
|
||||
|
||||
test('a missing or non-finite angle never passes', () => {
|
||||
const clean = {
|
||||
shown: true,
|
||||
moveEndMs: 8,
|
||||
revealMs: 80,
|
||||
dMoveEnd: 0,
|
||||
dReveal: 0,
|
||||
};
|
||||
assert.equal(arrivalRotationPassed(clean), true);
|
||||
for (const field of ['dMoveEnd', 'dReveal']) {
|
||||
assert.equal(arrivalRotationPassed({ ...clean, [field]: NaN }), false);
|
||||
assert.equal(
|
||||
arrivalRotationPassed({ ...clean, [field]: undefined }),
|
||||
false,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('a wrong angle on a late reveal frame is final, not retried into a pass', async () => {
|
||||
const f = retryFixture();
|
||||
await settle();
|
||||
f.frame(100, { shown: true, rotationError: 0 });
|
||||
// Shown at 600 ms (500 ms after settle) still wearing the tampered nose.
|
||||
f.frame(600, { shown: true, rotationError: 1.5 });
|
||||
await settle();
|
||||
f.frame(608, { shown: true, rotationError: 0 });
|
||||
f.frame(688, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /reveal.*500 ms/);
|
||||
assert.equal(result.dReveal, 1.5);
|
||||
assert.equal(result.attempts, 1);
|
||||
assert.equal(f.resources.moveEnds, 1);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('a wrong angle on a late settle frame is final, not retried into a pass', async () => {
|
||||
const f = retryFixture();
|
||||
await settle();
|
||||
f.frame(926, { shown: true, rotationError: 1.5 });
|
||||
await settle();
|
||||
f.frame(934, { shown: true, rotationError: 0 });
|
||||
f.frame(1014, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.match(result.error, /moveEnd.*500 ms/);
|
||||
assert.equal(result.dMoveEnd, 1.5);
|
||||
assert.equal(result.attempts, 1);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('a late hidden reveal frame records no angle and stays retryable', async () => {
|
||||
const f = retryFixture();
|
||||
await settle();
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
// Still hidden, still tampered: not a measured reveal angle.
|
||||
f.frame(600, { shown: false });
|
||||
const first = f.observation;
|
||||
await settle();
|
||||
f.frame(608, { shown: true, rotationError: 0 });
|
||||
f.frame(688, { shown: true, rotationError: 0 });
|
||||
const result = await first;
|
||||
assert.equal(result.attempts, 2);
|
||||
assert.equal(result.dReveal, 0);
|
||||
assert.equal(arrivalRotationPassed(result), true);
|
||||
});
|
||||
|
||||
test('a late frame with correct angles stays an inconclusive, retryable timeout', async () => {
|
||||
const f = retryFixture();
|
||||
await settle();
|
||||
f.frame(926, { shown: true, rotationError: 0 });
|
||||
await settle();
|
||||
f.frame(934, { shown: true, rotationError: 0 });
|
||||
f.frame(1014, { shown: true, rotationError: 0 });
|
||||
const result = await f.observation;
|
||||
assert.equal(result.attempts, 2);
|
||||
assert.equal(arrivalRotationPassed(result), true);
|
||||
});
|
||||
|
||||
test('a wrong first reveal on time is final: no retry', async () => {
|
||||
const f = retryFixture();
|
||||
await settle();
|
||||
f.frame(8, { shown: true, rotationError: 0 });
|
||||
f.frame(88, { shown: true, rotationError: 1.5 });
|
||||
const result = await f.observation;
|
||||
assert.equal(result.error, undefined);
|
||||
assert.equal(result.attempts, 1);
|
||||
assert.equal(f.resources.moveEnds, 1);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('maxAttempts bounds inconclusive retries', async () => {
|
||||
const f = retryFixture({ maxAttempts: 3 });
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
await settle();
|
||||
f.expire(attempt * 500);
|
||||
}
|
||||
const result = await f.observation;
|
||||
assert.equal(result.attempts, 3);
|
||||
assert.equal(f.counts.prepares, 3);
|
||||
assert.equal(arrivalRotationPassed(result), false);
|
||||
});
|
||||
|
||||
test('the 10° limit is inclusive', () => {
|
||||
const limit = Math.PI / 18;
|
||||
assert.equal(arrivalAngleViolated({ dMoveEnd: limit }), true);
|
||||
assert.equal(arrivalAngleViolated({ dReveal: limit * 0.999 }), false);
|
||||
});
|
||||
|
||||
test('every name the track harness imports from this module is exported', () => {
|
||||
const harness = readFileSync(
|
||||
new URL('../../scripts/track-regression.mjs', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const names = [
|
||||
...harness.matchAll(
|
||||
/(?:import|const)\s*\{([^}]+)\}\s*(?:from\s*|=\s*await import\()[^;]*trackArrivalObservation\.mjs/g,
|
||||
),
|
||||
].flatMap((match) => match[1].split(',').map((name) => name.trim()));
|
||||
assert.ok(names.includes('arrivalRotationPassed'));
|
||||
assert.ok(names.includes('observeArrivalRotationWithRetry'));
|
||||
for (const name of names)
|
||||
assert.equal(typeof arrivalModule[name], 'function', name);
|
||||
});
|
||||
Reference in new issue
Block a user