fix(udpscope): carry warm-up state across the hrt handover
An undeclared-rate accumulated scalar is served by packetBurst until HrtRateFit is ready, then by the hrt branch. The two place a burst differently -- packetBurst ends it at wallNow, the hrt branch at wallNow - (nElems-1)*hrtDt -- and the warm-up left no state behind, so the handover packet skipped the monotonic clamp and stepped the signal backwards by up to a burst width (-6.5 ms at 10 samples per 2.5 ms packet, -0.99 s at 1000 samples per 10 ms). Seeding lastEmitted* alone would only restore ordering. Without lastAccHrt/prevAccCount the first hrt packet also has no tick delta to measure, falls back to kDefaultDt and latches ClockOffset against a burst width that is wrong whenever the cadence is not 1 kHz -- 89 ms of permanent displacement at 100 samples per 10 ms, below the recalibration threshold that would otherwise heal it. Seed both. Also close the wallElapsed <= 0 bypass in both branches: skipping the bleed cap when the wall has not moved hands back the full proportional advance, letting a run of same-tick arrivals gain lead while no wall time passes at all. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
440b805afd
commit
3add2c42b9
@@ -328,15 +328,25 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
|
||||
if (factor < kMinBleedFactor) { factor = kMinBleedFactor; }
|
||||
double advance = nominal * factor;
|
||||
|
||||
/* A non-positive elapsed means the wall has not moved
|
||||
* since this signal's previous burst — a coarse arrival
|
||||
* clock, or two packets stamped within one tick of it.
|
||||
* There is no wall time to spend, so the cap is zero.
|
||||
* Skipping the cap in that case (which is what this code
|
||||
* used to do) hands back the full proportional advance,
|
||||
* so a run of same-tick arrivals gains lead while no wall
|
||||
* time passes at all — the divergence the cap exists to
|
||||
* stop, in its purest form. */
|
||||
const double wallElapsed = wallNow - st.lastEmittedWall;
|
||||
if (wallElapsed > 0.0) {
|
||||
const double cap = kWallBleedFraction * wallElapsed;
|
||||
if (cap < advance) { advance = cap; }
|
||||
}
|
||||
const double cap = (wallElapsed > 0.0)
|
||||
? (kWallBleedFraction * wallElapsed)
|
||||
: 0.0;
|
||||
if (cap < advance) { advance = cap; }
|
||||
step = advance / static_cast<double>(nElems);
|
||||
/* Unreachable with a finite positive dt — kept because
|
||||
* downstream monotonicity must not depend on that
|
||||
* argument holding for every value off the wire. */
|
||||
/* Reached whenever the cap is zero, and a backstop
|
||||
* against a nonsensical dt off the wire: downstream
|
||||
* requires strictly increasing stamps, so the burst must
|
||||
* still advance by something. */
|
||||
if (!(step > 0.0)) { step = dt * kMinBleedFactor; }
|
||||
base = st.lastEmittedEnd + step;
|
||||
}
|
||||
@@ -356,7 +366,37 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
|
||||
|
||||
/* No declared rate: need hrt-derived dt. */
|
||||
if (!hrtFit_.ready() || f.hrt == 0u) {
|
||||
return packetBurst(idx, nElems, wallNow, tsOut);
|
||||
const bool ok = packetBurst(idx, nElems, wallNow, tsOut);
|
||||
/* Carry the warm-up's state into the hrt branch, or the handover
|
||||
* from one to the other is a discontinuity in both directions.
|
||||
*
|
||||
* The producer-clock reference (lastAccHrt, prevAccCount) matters
|
||||
* most. Without it the first hrt packet has no previous tick to
|
||||
* subtract, falls back to kDefaultDt for its inter-element step and
|
||||
* latches ClockOffset against wallNow - (nElems-1)*kDefaultDt.
|
||||
* kDefaultDt is only right when the burst happens to run at 1 kHz;
|
||||
* at 100 samples per 10 ms packet it is ten times too wide and the
|
||||
* latch lands 89 ms in the past — permanently, since it is below
|
||||
* ClockOffset's recalibration threshold. Seeding here means the
|
||||
* first hrt packet measures a real tick delta and latches correctly.
|
||||
*
|
||||
* The emitted-timeline reference (lastEmitted*) then only has to
|
||||
* cover residual disagreement, but it is what keeps the handover
|
||||
* MONOTONIC: packetBurst ends its burst at wallNow while the hrt
|
||||
* branch ends at wallNow - (nElems-1)*hrtDt, and without a previous
|
||||
* end to clamp against the first hrt packet steps the signal
|
||||
* backwards by up to a whole burst width. */
|
||||
if (f.hrt != 0u) {
|
||||
st.lastAccHrt = f.hrt;
|
||||
st.lastAccValid = true;
|
||||
}
|
||||
st.prevAccCount = nElems;
|
||||
if (!ok) { return false; }
|
||||
st.lastEmittedEnd = tsOut[nElems - 1u];
|
||||
st.lastEmittedWall = wallNow;
|
||||
st.lastCounter = f.counter;
|
||||
st.lastEmittedValid = true;
|
||||
return true;
|
||||
}
|
||||
const double rate = hrtFit_.ticksPerSecond();
|
||||
|
||||
@@ -455,11 +495,16 @@ bool FrameDecoder::timestamps(const FrameView& f, uint32_t idx,
|
||||
* at kWallBleedFraction: only that makes the lead bleed off. */
|
||||
if (st.lastEmittedValid && base <= st.lastEmittedEnd) {
|
||||
const double wallElapsed = wallNow - st.lastEmittedWall;
|
||||
if (wallElapsed > 0.0) {
|
||||
const double cap = kWallBleedFraction * wallElapsed /
|
||||
static_cast<double>(nElems);
|
||||
if (cap < step) { step = cap; }
|
||||
}
|
||||
/* No wall movement, no wall time to spend: see the same cap in the
|
||||
* declared branch. Zero rather than "skip the cap", so a run of
|
||||
* same-tick arrivals cannot advance a full hrtDt per sample while
|
||||
* the wall stands still. */
|
||||
const double cap = (wallElapsed > 0.0)
|
||||
? (kWallBleedFraction * wallElapsed /
|
||||
static_cast<double>(nElems))
|
||||
: 0.0;
|
||||
if (cap < step) { step = cap; }
|
||||
if (!(step > 0.0)) { step = hrtDt * kMinBleedFactor; }
|
||||
base = st.lastEmittedEnd + step;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user