feat(phase-lock): controller v3 — grid-locked submits, arrival sensing, antipode damping
ci / docs-site (push) Successful in 1m17s
ci / web (push) Successful in 2m14s
ci / rust-arm64 (push) Successful in 2m54s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 19s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 25s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 13s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
deb / build-publish-client-arm64 (push) Successful in 2m59s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
apple / swift (push) Successful in 4m48s
deb / build-publish-host (push) Successful in 4m51s
android / android (push) Successful in 5m31s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 32s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 31s
docker / builders-arm64cross (push) Successful in 12s
docker / deploy-docs (push) Successful in 35s
deb / build-publish (push) Successful in 6m0s
ci / rust (push) Successful in 9m53s
arch / build-publish (push) Successful in 10m32s
windows-host / package (push) Successful in 19m10s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m2s
apple / screenshots (push) Successful in 21m3s
ci / docs-site (push) Successful in 1m17s
ci / web (push) Successful in 2m14s
ci / rust-arm64 (push) Successful in 2m54s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 19s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 25s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 13s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
deb / build-publish-client-arm64 (push) Successful in 2m59s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 13s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
apple / swift (push) Successful in 4m48s
deb / build-publish-host (push) Successful in 4m51s
android / android (push) Successful in 5m31s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 32s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 31s
docker / builders-arm64cross (push) Successful in 12s
docker / deploy-docs (push) Successful in 35s
deb / build-publish (push) Successful in 6m0s
ci / rust (push) Successful in 9m53s
arch / build-publish (push) Successful in 10m32s
windows-host / package (push) Successful in 19m10s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 15s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m17s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m2s
apple / screenshots (push) Successful in 21m3s
Midday on-glass falsified v2's remaining assumptions: a per-frame additive hold SATURATES the arrival-slaved loop once hold + work >= interval — submits free-run at their own cadence and the commanded phase dissolves (measured: +-2 ms hold steps, zero client response) — and the latch statistic hovers at the target's ANTIPODE, where shortest-way errors sign-flip on noise (the 0<->2<->4 ms offset chatter). v3, one change per falsified assumption: - ACTUATOR: submits lock to an absolute grid (epoch + k x period + offset); the controller walks only the offset. A periodic grid cannot free-run — occupancy is one frame per period whatever the offset — so phase actuation is linear BY CONSTRUCTION. Disengaged = no grid sleeps = zero cost; every failure path DISENGAGES (never parks: v2's e2e-tax lesson). - SENSOR: the client reports the circular mean + coherence of the ARRIVAL lead (per-AU reassembly stamps vs the panel latch grid) — the phase the host actually controls; latch measured downstream of the decoder pipeline, which absorbed the actuation. pf.phase logs "arrival lead circ= coh=". - ANTIPODE DAMPING: errors within 1 ms of +-period/2 take half-steps until they commit to a side. Harness rewritten around plants glass validated: a GRID plant (linear by construction), the DECOUPLED plant (v2's saturation — must disengage, not orbit or park), an antipode start (must converge within one period of travel), incoherence (never engages), a regime change (re-engages after backoff), and the actuator's own periodicity/offset-linearity. Also fixes the harness's SIM_TARGET (a mis-derived max asserted 3.5 ms where the controller's actual target is 2.5 — the controller was right, the ruler was wrong; the Python cross-check inherited the same constant, a lesson in replicating the CODE, not the author's belief about it). Gates: v3 simulations 6/6; host bin suite 345/1 (the documented environmental qemu UDP test); clippy --all-targets -D warnings clean; cargo ndk arm64 check clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,8 @@ pub(super) fn run_async(
|
||||
|
||||
let mut free_inputs: VecDeque<usize> = VecDeque::new();
|
||||
let mut pending_aus: VecDeque<Frame> = VecDeque::new();
|
||||
// Phase-lock v3: per-AU arrival stamps for the circular arrival-lead report (drained 1 Hz).
|
||||
let mut arrival_stamps: Vec<i128> = Vec::new();
|
||||
let mut ready: Vec<OutputReady> = Vec::new();
|
||||
let mut applied_ds: Option<DataSpace> = None;
|
||||
let mut fed: u64 = 0;
|
||||
@@ -321,6 +323,7 @@ pub(super) fn run_async(
|
||||
&mut fatal,
|
||||
&mut gate,
|
||||
&mut recovery_flags,
|
||||
&mut arrival_stamps,
|
||||
));
|
||||
}
|
||||
// Coalesce every other event already queued into this one work pass — correct newest-only
|
||||
@@ -336,6 +339,7 @@ pub(super) fn run_async(
|
||||
&mut fatal,
|
||||
&mut gate,
|
||||
&mut recovery_flags,
|
||||
&mut arrival_stamps,
|
||||
));
|
||||
}
|
||||
if vsync_tick {
|
||||
@@ -380,28 +384,50 @@ pub(super) fn run_async(
|
||||
if p.pump(&codec, clock, &tracker, &stats, now_monotonic_ns()) {
|
||||
rendered += 1;
|
||||
}
|
||||
// The 1 Hz window flush doubles as the phase-lock report tick: the CIRCULAR
|
||||
// (vector-mean) latch phase + coherence are the host capture controller's v2 error
|
||||
// signal (design/phase-locked-capture.md §6; a median is immovable under jitter).
|
||||
// Timestamps convert monotonic→realtime→host — the skew offset lives client-side.
|
||||
if let (Some((circ_latch_ns, coherence)), Some(c)) = (p.flush_log(&meter, clock), clock)
|
||||
{
|
||||
// The 1 Hz window flush doubles as the phase-lock report tick. v3 sensor: the
|
||||
// CIRCULAR mean + coherence of the ARRIVAL lead — each AU's reassembly stamp
|
||||
// against the panel's latch grid — because arrival is the phase the host actually
|
||||
// controls; the v2 latch statistic measured downstream of the decoder pipeline,
|
||||
// which absorbed the actuation (on-glass 2026-07-31). Timestamps convert
|
||||
// monotonic→realtime→host — the skew offset lives client-side.
|
||||
if let (Some(_), Some(c)) = (p.flush_log(&meter, clock), clock) {
|
||||
let period = c.panel_period_ns().max(c.period_ns());
|
||||
if period > 0 {
|
||||
if let Some(t) = c.next_target(now_monotonic_ns(), 0) {
|
||||
let mono_now = now_monotonic_ns();
|
||||
let real_now = now_realtime_ns();
|
||||
let latch_real_ns = real_now + (t.expected_present_ns - mono_now) as i128;
|
||||
let latch_host_ns = (latch_real_ns
|
||||
+ clock_offset.load(Ordering::Relaxed) as i128)
|
||||
.max(0) as u64;
|
||||
client.report_phase(
|
||||
latch_host_ns,
|
||||
period.clamp(0, u32::MAX as i64) as u32,
|
||||
1_000_000, // skew residual + latch jitter — conservative 1 ms
|
||||
circ_latch_ns.min(u32::MAX as u64) as u32,
|
||||
coherence,
|
||||
);
|
||||
let leads_us: Vec<u64> = arrival_stamps
|
||||
.iter()
|
||||
.map(|&r_ns| {
|
||||
let arrival_mono = mono_now as i128 - (real_now - r_ns);
|
||||
((t.expected_present_ns as i128 - arrival_mono)
|
||||
.rem_euclid(period as i128)
|
||||
/ 1000) as u64
|
||||
})
|
||||
.collect();
|
||||
arrival_stamps.clear();
|
||||
if let Some((lead_mean_ns, coherence)) =
|
||||
punktfunk_core::phase::circular_latch(&leads_us, period)
|
||||
{
|
||||
log::info!(
|
||||
target: "pf.phase",
|
||||
"arrival lead circ={:.2}ms coh={}",
|
||||
lead_mean_ns as f64 / 1e6,
|
||||
coherence
|
||||
);
|
||||
let latch_real_ns =
|
||||
real_now + (t.expected_present_ns - mono_now) as i128;
|
||||
let latch_host_ns = (latch_real_ns
|
||||
+ clock_offset.load(Ordering::Relaxed) as i128)
|
||||
.max(0) as u64;
|
||||
client.report_phase(
|
||||
latch_host_ns,
|
||||
period.clamp(0, u32::MAX as i64) as u32,
|
||||
1_000_000, // skew residual — conservative 1 ms
|
||||
lead_mean_ns.min(u32::MAX as u64) as u32,
|
||||
coherence,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -634,6 +660,7 @@ fn dispatch_event(
|
||||
fatal: &mut bool,
|
||||
gate: &mut ReanchorGate,
|
||||
recovery_flags: &mut VecDeque<(u64, u32)>,
|
||||
arrival_stamps: &mut Vec<i128>,
|
||||
) -> bool {
|
||||
match ev {
|
||||
DecodeEvent::Au(f, gap) => {
|
||||
@@ -646,6 +673,17 @@ fn dispatch_event(
|
||||
if recovery_flags.len() > IN_FLIGHT_CAP {
|
||||
recovery_flags.pop_front();
|
||||
}
|
||||
// Phase-lock v3 sensor: the ARRIVAL stamp (reassembly completion, realtime) — the
|
||||
// phase the host actually controls. The latch-based v2 sensor measured downstream
|
||||
// of the decoder pipeline, which absorbed the host's actuation (on-glass 07-31).
|
||||
arrival_stamps.push(if f.received_ns > 0 {
|
||||
f.received_ns as i128
|
||||
} else {
|
||||
now_realtime_ns()
|
||||
});
|
||||
if arrival_stamps.len() > 256 {
|
||||
arrival_stamps.remove(0);
|
||||
}
|
||||
pending_aus.push_back(f);
|
||||
if pending_aus.len() > FRAME_PARK_CAP {
|
||||
pending_aus.pop_front(); // sustained overflow — drop oldest, signal a keyframe request
|
||||
|
||||
Reference in New Issue
Block a user