From e3c4e88d6f51db58462cb22937e5853ff25ddddd Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Wed, 21 Oct 2026 15:21:43 +0100 Subject: [PATCH] test(track): freshen rotation passes before the arrival probe and retry an inconclusive timeout A headless SwiftShader run showed a 926 ms first frame on the military probe, which no wall-clock budget can accept: the 1 s idle refresh may already have run inside it. Raise moveEnd and wait one frame before each attempt so the settle frame is measured against a pass that just ran, and treat a timeout (never a wrong angle) as inconclusive with one more attempt. Document why the per-phase budget stays at half the refresh interval. Co-Authored-By: Claude Fable 5.1 --- scripts/track-regression.mjs | 61 +++++++++++++++---------- src/tooling/trackArrivalObservation.mjs | 7 ++- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/scripts/track-regression.mjs b/scripts/track-regression.mjs index 4432643ac..44ba9badd 100644 --- a/scripts/track-regression.mjs +++ b/scripts/track-regression.mjs @@ -2770,28 +2770,43 @@ async function main() { const bb = findBB(id); if (!bb || !bb.show) return { error: `${id} missing/hidden` }; const r0 = bb.rotation; // settled tamper origin; correctness uses a fresh projection below - const observed = await observeArrivalRotation({ - scene: v.scene, - startMoveEnd() { - // Only moveEnd can repair the tampered angle at the idle pose. - bb.rotation = r0 + 1.5; - v.camera.moveEnd.raiseEvent(); - }, - startReveal() { - // Hide immediately after the confirmed settle frame. The next - // eligible fleet tick must reveal AND re-aim in that same tick. - bb.rotation = r0 + 1.5; - bb.show = false; - }, - read() { - return { - shown: bb.show, - rotationError: angDiff(bb.rotation, - screenProjectedRotation(v.scene, bb.position, course, null)), - }; - }, - }); const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id)); + // A wall-clock timeout is inconclusive, not a wrong angle: the frame + // landed where the 1 s idle refresh could already have repaired it + // (seen once on headless SwiftShader: a 926 ms first frame). Such an + // observation gets one more attempt; a wrong first angle never does. + let observed; + for (let attempt = 1; attempt <= 2; attempt++) { + // Freshen both layers' rotation passes right before the probe, so the + // settle frame is measured against a pass that just ran rather than + // one left over from the previous probe — with the camera idle, a + // layer otherwise runs a pass only every ROTATION_REFRESH_MS. + v.camera.moveEnd.raiseEvent(); + await nextFrames(1); + observed = await observeArrivalRotation({ + scene: v.scene, + startMoveEnd() { + // Only moveEnd can repair the tampered angle at the idle pose. + bb.rotation = r0 + 1.5; + v.camera.moveEnd.raiseEvent(); + }, + startReveal() { + // Hide immediately after the confirmed settle frame. The next + // eligible fleet tick must reveal AND re-aim in that same tick. + bb.rotation = r0 + 1.5; + bb.show = false; + }, + read() { + return { + shown: bb.show, + rotationError: angDiff(bb.rotation, + screenProjectedRotation(v.scene, bb.position, course, null)), + }; + }, + }); + observed.attempts = attempt; + if (!observed.error) break; + } return { r0, ...observed, hasModel }; }; const flightCourse = window.__SYNTH.flights.find((f) => f.icao === 'aaa002')?.track ?? 0; @@ -2801,8 +2816,8 @@ async function main() { military: await probe('bbb101', militaryCourse), }; }); - const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? `${a.error}; elapsed=${Number(a.elapsedMs).toFixed(1)} ms` - : `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° shown=${a.shown} flipFrame=${a.flipFrame} settle=${a.moveEndMs.toFixed(1)} ms reveal=${a.revealMs.toFixed(1)} ms total=${a.elapsedMs.toFixed(1)} ms hasModel=${a.hasModel}`); + const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? `${a.error}; elapsed=${Number(a.elapsedMs).toFixed(1)} ms attempts=${a.attempts}` + : `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° shown=${a.shown} flipFrame=${a.flipFrame} settle=${a.moveEndMs.toFixed(1)} ms reveal=${a.revealMs.toFixed(1)} ms total=${a.elapsedMs.toFixed(1)} ms attempts=${a.attempts} hasModel=${a.hasModel}`); const TAMPER_DEG = 1.5 * 180 / Math.PI; // ≈ 86° — an uncorrected nose stays this far off record(`arrival: moveEnd + first reveal both re-aim before idle refresh, flights (tamper was ${TAMPER_DEG.toFixed(0)}°)`, arrivalRotationPassed(arrival.flights), fmtArr(arrival.flights)); diff --git a/src/tooling/trackArrivalObservation.mjs b/src/tooling/trackArrivalObservation.mjs index 26d29b5c5..7fa2d29c4 100644 --- a/src/tooling/trackArrivalObservation.mjs +++ b/src/tooling/trackArrivalObservation.mjs @@ -2,8 +2,11 @@ * 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 — while a - * ~300 ms software-rendered frame (headless SwiftShader) still fits twice. + * 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;