From 338d94197c36b30af09377ba4da7fc5dfc181461 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 3 Oct 2026 11:51:30 -0700 Subject: [PATCH] Settle frame pacing after an inconsistent vsync gate (#2197) --- .../angelica/rendering/PacerCore.java | 5 +- .../angelica/rendering/PacerCoreTest.java | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/PacerCore.java b/src/main/java/com/gtnewhorizons/angelica/rendering/PacerCore.java index d616847fc..e8556eac8 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/PacerCore.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/PacerCore.java @@ -63,6 +63,7 @@ final class PacerCore { private boolean locked; private boolean probing; private boolean probeExhausted; + private boolean gateAbandoned; private long probeLeadNanos; private boolean haveCandidate; private long candidateEndNanos; @@ -336,7 +337,7 @@ final class PacerCore { if (locked && framesSinceCandidate >= LOCK_FRAMES) dropLock(); return; } - if (haveCandidate && framesSinceCandidate <= LOCK_WINDOW) { + if (!gateAbandoned && haveCandidate && framesSinceCandidate <= LOCK_WINDOW) { final long delta = sampleEnd - candidateEndNanos; final long k = (delta + refreshPeriodNanos / 2) / refreshPeriodNanos; if (k >= 1L && Math.abs(delta - k * refreshPeriodNanos) <= refreshPeriodNanos / LOCK_TOL_DIVISOR) { @@ -347,6 +348,7 @@ final class PacerCore { if (++inconsistentPairs >= LOCK_WINDOW) { dropLock(); probeExhausted = true; + gateAbandoned = true; } } } @@ -358,6 +360,7 @@ final class PacerCore { private void armProbe() { probeLeadNanos = 0L; probeExhausted = false; + gateAbandoned = false; } private void dropLock() { diff --git a/src/test/java/com/gtnewhorizons/angelica/rendering/PacerCoreTest.java b/src/test/java/com/gtnewhorizons/angelica/rendering/PacerCoreTest.java index 5935a9ab9..68b51ce21 100644 --- a/src/test/java/com/gtnewhorizons/angelica/rendering/PacerCoreTest.java +++ b/src/test/java/com/gtnewhorizons/angelica/rendering/PacerCoreTest.java @@ -172,6 +172,51 @@ class PacerCoreTest { assertFalse(rig.core.probing(), "a gate proven off the grid must not be probed again"); } + @Test + void anAbandonedGateStaysRetiredThroughAnotherOnGridRun() { + final Rig rig = new Rig(P60, true, 120); + rig.feedRun(PacerCore.LOCK_PAIRS + 1, 20 * MS, 5 * MS, P60); + assertTrue(rig.core.locked()); + assertEquals(60, rig.core.effectiveCapHz()); + + long end = 20 * MS + (PacerCore.LOCK_PAIRS + 1) * P60; + for (int i = 0; i < PacerCore.LOCK_WINDOW + 1; i++) { + end += P60 / 2; + rig.feed(end, 5 * MS, end); + } + assertFalse(rig.core.locked()); + assertFalse(rig.core.probing()); + + for (int i = 0; i < PacerCore.LOCK_PAIRS + 1; i++) { + end += P60; + rig.feed(end, 5 * MS, end); + } + assertFalse(rig.core.locked(), "a retired inconsistent gate must not lock again"); + assertFalse(rig.core.probing()); + } + + @Test + void invalidateClearsAnAbandonedGate() { + final Rig rig = new Rig(P60, true, 120); + rig.feedRun(PacerCore.LOCK_PAIRS + 1, 20 * MS, 5 * MS, P60); + + long end = 20 * MS + (PacerCore.LOCK_PAIRS + 1) * P60; + for (int i = 0; i < PacerCore.LOCK_WINDOW + 1; i++) { + end += P60 / 2; + rig.feed(end, 5 * MS, end); + } + assertFalse(rig.core.locked()); + assertFalse(rig.core.probing()); + + rig.core.invalidate(); + for (int i = 0; i < PacerCore.LOCK_PAIRS + 1; i++) { + end += P60; + rig.feed(end, 5 * MS, end); + } + assertTrue(rig.core.locked()); + assertFalse(rig.core.probing()); + } + @Test void samplesFurtherApartThanTheLockWindowNeverPairUp() { final Rig rig = new Rig(P60, true, 0); @@ -372,6 +417,18 @@ class PacerCoreTest { assertEquals(P60, rig.presents.get(300) - rig.presents.get(299), "an expired probe must fall back to the exact cadence"); } + @Test + void anExpiredProbeLeadStillLocksWhenTheGateArrives() { + final Rig rig = new Rig(P60, true, 0); + for (int i = 0; i < 20; i++) rig.frame(4 * MS); + assertFalse(rig.core.locked()); + assertFalse(rig.core.probing()); + + rig.feedRun(PacerCore.LOCK_PAIRS + 1, 20 * MS, 5 * MS, P60); + assertTrue(rig.core.locked()); + assertFalse(rig.core.probing()); + } + @Test void mailboxIsNeverProbedAndNeverLocks() { final Rig rig = new Rig(P60, true, false, 0);