Project

General

Profile

User Story #61 » 0005-feat-netcode-batched-interpolated-renderPositions-th.patch

knight8241, 08/07/2026 18:32

View differences:

common/src/commonMain/kotlin/com/aether/mofe/engine/netcode/NetcodeSession.kt
import com.aether.mofe.math.Shape
import com.aether.mofe.model.DeviceId
import com.aether.mofe.model.Timestamp
import com.aether.mofe.model.Vector3D
/**
* The netcode integration seam — the stateful holder the wiring drives, keeping the
......
return poseAt(deviceId, Timestamp(nowMesh.microseconds - delay))
}
/**
* Entity-interpolated render POSITIONS for every device the session has history for,
* at [nowMesh] — the smooth, jitter-free source for the 3-D scene NODES, the position
* analogue of [MeshAimTracker.tracks]. Same buffers and the same quality-scaled
* interpolation delay as [renderPose]; a device without interpolable history yet is
* simply absent from the map. Reading this each render frame (≈60 Hz) resamples the
* smooth trajectory *between* the ~10 Hz solves, so the scene draws continuous motion
* instead of the raw per-solve jitter it draws today. The delay is computed once here,
* so every node in a frame shares one consistent render instant.
*/
fun renderPositions(nowMesh: Timestamp): Map<DeviceId, Vector3D> {
val delay = MeshQuality.interpDelayMicros(quality(nowMesh), baseInterpDelayMicros)
val renderAt = Timestamp(nowMesh.microseconds - delay)
return buffers.keys.mapNotNull { id ->
poseAt(id, renderAt)?.let { id to it.position }
}.toMap()
}
/**
* Leader-side arbitration: rewind the subject's authoritative history to the claimed
* instant, re-detect the transition of [shape] there, and decide. No history for the
common/src/commonTest/kotlin/com/aether/mofe/engine/netcode/NetcodeSessionTest.kt
import com.aether.mofe.model.Quaternion
import com.aether.mofe.model.Timestamp
import com.aether.mofe.model.Vector3D
import kotlin.math.cos
import kotlin.math.sin
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
......
assertEquals(EventVerdict.Reason.NO_TRANSITION, verdict.reason)
}
// ── renderPositions: the batched, interpolated source for the 3-D scene NODES (F1) ──────────
@Test
fun renderPositionsInterpolatesEveryTrackedDeviceAndSkipsEmptyBuffers() {
val s = syncedSession()
val a = DeviceId("a")
val b = DeviceId("b")
val empty = DeviceId("empty")
s.recordSample(a, Timestamp(0), pose(0.0))
s.recordSample(a, Timestamp(1_000_000), pose(10.0))
s.recordSample(b, Timestamp(0), TemporalPose(Vector3D.ZERO, Vector3D.ZERO, Quaternion.IDENTITY))
s.recordSample(b, Timestamp(1_000_000), TemporalPose(Vector3D(0.0, 4.0, 0.0), Vector3D.ZERO, Quaternion.IDENTITY))
s.bufferFor(empty) // buffer exists but holds no samples
// GOOD clock ⇒ 100 ms delay ⇒ render at 900 ms: a→x=9, b→y=3.6, all in one consistent instant.
val positions = s.renderPositions(Timestamp(1_000_000))
assertEquals(9.0, positions.getValue(a).x, 1e-9)
assertEquals(3.6, positions.getValue(b).y, 1e-9)
assertFalse(positions.containsKey(empty), "an empty buffer yields no ghost node at the origin")
assertEquals(2, positions.size)
}
@Test
fun renderPositionsGiveTheSceneASmoothStreamVsRawJitter() {
// The F1 premise: the 3-D scene must consume interpolated positions, not the raw ~10 Hz solve.
// A target ~10 m out drifting +Y at 1 m/s, sampled at 10 Hz with deterministic ±5 cm / ±3 cm
// jitter — the same shape the AoA solve delivers. renderPositions read at ~60 Hz must trace a
// far smoother path than the raw per-solve stream a naive scene would draw.
val s = syncedSession()
val dev = DeviceId("t")
fun sampleAt(t: Long) = Vector3D(10.0, 1.0 * (t / 1_000_000.0), 0.0) +
Vector3D(0.0, 0.05 * sin(t / 60_000.0), 0.03 * cos(t / 71_000.0))
var t = 0L
while (t <= 900_000) {
s.recordSample(dev, Timestamp(t), TemporalPose(sampleAt(t), Vector3D(0.0, 1.0, 0.0), Quaternion.IDENTITY))
t += 100_000 // 10 Hz
}
// Control: the frame-to-frame jump a raw-snapshot scene would draw across the read span.
var maxRaw = 0.0
var prevRaw: Vector3D? = null
var rt = 200_000L
while (rt <= 700_000) { // the samples the render read (now−100 ms) traverses below
val p = sampleAt(rt)
prevRaw?.let { maxRaw = maxOf(maxRaw, it.distanceTo(p)) }
prevRaw = p
rt += 100_000
}
// The scene stream: renderPositions at ~60 Hz. GOOD clock ⇒ 100 ms delay, so now∈[300k,800k]
// reads at [200k,700k] — strictly interpolating between samples, never clamped to an edge.
var maxRender = 0.0
var prevRender: Vector3D? = null
var now = 300_000L
while (now <= 800_000) {
val p = s.renderPositions(Timestamp(now)).getValue(dev)
prevRender?.let { maxRender = maxOf(maxRender, it.distanceTo(p)) }
prevRender = p
now += 16_666 // ~60 Hz
}
assertTrue(maxRaw > 0.08, "control: the raw 10 Hz stream really does jump ($maxRaw m/frame)")
assertTrue(
maxRender < 0.4 * maxRaw,
"interpolated scene stream is far smoother — render ${maxRender} m/frame vs raw $maxRaw m/frame",
)
}
@Test
fun clockConversionsDelegateToMeshClock() {
val s = NetcodeSession()
(1-1/2)