Check tracking rotation on the first visible frame within fleet cadence

This commit is contained in:
Said committed 2026-10-19 23:16:02 +01:00
1 parent d90bae49b6
commit f927ef288a
2 files changed
+56 -26

No files matched your search

@@ -1,3 +1,6 @@
- Make CCTV QA ground-sampling checks respect the active terrain regime and - Make CCTV QA ground-sampling checks respect the active terrain regime and
frame the actual calibration gizmo for the drag regression, preserving the frame the actual calibration gizmo for the drag regression, preserving the
photoreal completion floor and all drag invariants. photoreal completion floor and all drag invariants.
- Measure the tracking arrival regression on the first visible frame within
250 ms, accounting for the 80 ms fleet cadence while rejecting a stale
first-visible orientation instead of retrying until it is correct.
+53 -26
View File
@@ -2696,9 +2696,9 @@ async function main() {
// Staged deterministically: park the camera nadir over the synthetic // Staged deterministically: park the camera nadir over the synthetic
// cluster, let rotations settle, TAMPER a billboard's rotation, then // cluster, let rotations settle, TAMPER a billboard's rotation, then
// trigger each path with the camera idle. The idle-drift catch-up // trigger each path with the camera idle. The idle-drift catch-up
// (ROTATION_REFRESH_MS = 1 s) can coincidentally mask a regression in a // (ROTATION_REFRESH_MS = 1 s) must not repair a stale first-visible nose
// ~300 ms SwiftShader frame window, so a regression shows as an // before we inspect it: capture that first postRender, with a 250 ms
// intermittent (not guaranteed) failure — never a false failure. // deadline that admits the 80 ms fleet cadence without waiting a second.
// ============================================================ // ============================================================
console.log('\nChange 4 — arrival rotation: moveEnd settle pass + horizon-reveal pass'); console.log('\nChange 4 — arrival rotation: moveEnd settle pass + horizon-reveal pass');
const arrival = await evalPage(async () => { const arrival = await evalPage(async () => {
@@ -2762,6 +2762,43 @@ async function main() {
}); });
await nextFrames(3); await nextFrames(3);
const angDiff = (a, b) => Math.abs(Math.atan2(Math.sin(a - b), Math.cos(a - b))); const angDiff = (a, b) => Math.abs(Math.atan2(Math.sin(a - b), Math.cos(a - b)));
// Render frames are not fleet ticks: at 60/120 Hz four frames can
// precede the next 80 ms fleet pass. Observe elapsed time instead, and
// freeze the angle on the FIRST visible frame, even when it is wrong.
const observeFirstReveal = (bb, course, budgetMs = 250) => new Promise((resolve) => {
const startedAt = performance.now();
let frames = 0;
let settled = false;
const finish = (shown, elapsedMs) => {
if (settled) return;
settled = true;
clearTimeout(timer);
remove();
const expected = shown
? screenProjectedRotation(v.scene, bb.position, course, null)
: null;
resolve({
shown,
flipFrame: shown ? frames : -1,
revealElapsedMs: elapsedMs,
revealWithinBudget: shown && elapsedMs <= budgetMs,
dReveal: shown && Number.isFinite(expected) ? angDiff(bb.rotation, expected) : null,
});
};
const remove = v.scene.postRender.addEventListener(() => {
frames += 1;
const elapsedMs = performance.now() - startedAt;
if (bb.show) {
finish(true, elapsedMs);
} else if (elapsedMs >= budgetMs) {
finish(false, elapsedMs);
} else {
v.scene.requestRender();
}
});
const timer = setTimeout(() => finish(false, performance.now() - startedAt), budgetMs);
v.scene.requestRender();
});
const probe = async (id, course) => { const probe = async (id, course) => {
const bb = findBB(id); const bb = findBB(id);
if (!bb || !bb.show) return { error: `${id} missing/hidden` }; if (!bb || !bb.show) return { error: `${id} missing/hidden` };
@@ -2779,25 +2816,17 @@ async function main() {
const expectedMoveEnd = screenProjectedRotation(v.scene, bb.position, course, null); const expectedMoveEnd = screenProjectedRotation(v.scene, bb.position, course, null);
// (b) reveal pass: tamper + hide — the next fleet tick must flip it // (b) reveal pass: tamper + hide — the next fleet tick must flip it
// visible AND correct the nose in that same tick (camera still idle, // visible AND correct the nose in that same tick (camera still idle,
// no moveEnd raised). Diagnostic fields (round 5): record WHICH frame // no moveEnd raised). Record frame count as diagnostics only: the
// the flip landed on (flipFrame; -1 = never within the extended // first visible postRender must already have the corrected rotation.
// window) and whether a 3D model owns this id — distinguishes "tick
// slow under load" from "occluder says hidden" from "model handoff".
bb.rotation = r0 + 1.5; bb.rotation = r0 + 1.5;
bb.show = false; bb.show = false;
let flipFrame = -1; const reveal = await observeFirstReveal(bb, course);
for (let f = 1; f <= 6; f++) {
await nextFrames(1);
if (bb.show && flipFrame === -1) flipFrame = f;
if (f >= 2 && flipFrame !== -1) break;
}
const expectedReveal = screenProjectedRotation(v.scene, bb.position, course, null);
const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id)); const hasModel = !!(window.__g3dFindModel && window.__g3dFindModel(id));
return { return {
r0, r0,
dMoveEnd: angDiff(afterMoveEnd, expectedMoveEnd), dMoveEnd: angDiff(afterMoveEnd, expectedMoveEnd),
dReveal: angDiff(bb.rotation, expectedReveal), ...reveal,
shown: bb.show, flipFrame, hasModel, hasModel,
}; };
}; };
const flightCourse = window.__SYNTH.flights.find((f) => f.icao === 'aaa002')?.track ?? 0; const flightCourse = window.__SYNTH.flights.find((f) => f.icao === 'aaa002')?.track ?? 0;
@@ -2808,20 +2837,18 @@ async function main() {
}; };
}); });
const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? a.error const fmtArr = (a) => (!a ? `no result (${arrival?.error || 'phase error'})` : a.error ? a.error
: `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${(a.dReveal * 180 / Math.PI).toFixed(1)}° shown=${a.shown} flipFrame=${a.flipFrame} hasModel=${a.hasModel}`); : `settle-err=${(a.dMoveEnd * 180 / Math.PI).toFixed(1)}° reveal-err=${a.dReveal === null ? 'unobserved' : (a.dReveal * 180 / Math.PI).toFixed(1) + '°'} shown=${a.shown} flipFrame=${a.flipFrame} revealMs=${a.revealElapsedMs.toFixed(1)} hasModel=${a.hasModel}`);
const TAMPER_DEG = 1.5 * 180 / Math.PI; // ≈ 86° — an uncorrected nose stays this far off const TAMPER_DEG = 1.5 * 180 / Math.PI; // ≈ 86° — an uncorrected nose stays this far off
// Reveal budget re-pinned 2 → 4 frames (round 5): the diagnostic probe // 250 ms spans multiple 80 ms fleet opportunities while remaining well
// shows the military flip consistently lands on frame 2 — the exact edge // below the 1 s idle catch-up. Never retry until the angle becomes correct:
// of the old budget — so any load hiccup flaked it (flights lands frame // dReveal is the immutable first-visible-frame observation above.
// 1). The regression this protects against is a reveal that waits for
// the 1 s idle catch-up (~30-60 frames) or never re-aims; 4 frames still
// catches that with a wide margin, with zero-headroom flakes gone.
const arrOk = (a) => !!a && !a.error && a.shown const arrOk = (a) => !!a && !a.error && a.shown
&& (a.flipFrame === -1 ? false : a.flipFrame <= 4) && a.revealWithinBudget && a.revealElapsedMs <= 250
&& a.dReveal !== null
&& a.dMoveEnd * 180 / Math.PI < 10 && a.dReveal * 180 / Math.PI < 10; && a.dMoveEnd * 180 / Math.PI < 10 && a.dReveal * 180 / Math.PI < 10;
record(`arrival: moveEnd + reveal both re-aim within 4 frames, flights (tamper was ${TAMPER_DEG.toFixed(0)}°)`, record(`arrival: moveEnd re-aims and first reveal is correct within 250 ms, flights (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
arrOk(arrival.flights), fmtArr(arrival.flights)); arrOk(arrival.flights), fmtArr(arrival.flights));
record(`arrival: moveEnd + reveal both re-aim within 4 frames, military (tamper was ${TAMPER_DEG.toFixed(0)}°)`, record(`arrival: moveEnd re-aims and first reveal is correct within 250 ms, military (tamper was ${TAMPER_DEG.toFixed(0)}°)`,
arrOk(arrival.military), fmtArr(arrival.military)); arrOk(arrival.military), fmtArr(arrival.military));
// ============================================================ // ============================================================