Draw the joined player's arena to the frame's clock, not the wire's - #27
Open
sbddesign wants to merge 2 commits into
Open
Draw the joined player's arena to the frame's clock, not the wire's#27sbddesign wants to merge 2 commits into
sbddesign wants to merge 2 commits into
Conversation
The first two-device test of #25/#26 connected, flew, and was jerky on the joined player while the host was smooth. The host's pairs of poses advance exactly once per tick by construction. The client's did not: it applied every snapshot the moment it arrived, and the frame then blended between the last two poses at a factor taken from the *local* clock -- so a pair that advanced twice between frames jumped, and one that had not advanced slid backwards. Now the client applies one snapshot per tick of its own. `createClient` holds what arrives early in a queue sorted by tick; a tick that finds nothing to apply coasts (`Game.coast`, new: every hull and bolt the mirror does not fly itself, carried one tick along its last velocity -- the same integration `Ship.step` and the bolt pool do -- which on straight flight lands where the missing snapshot would have); a gap at the head of the queue is coasted once before it is applied, so a snapshot reordered past its predecessor waits for it instead of skipping it, and a lost one is a coast rather than a jump; and a queue deeper than `SNAPSHOT_DEPTH` (four ticks) drains two a tick, bounded at `SNAPSHOT_QUEUE`. The queue fills only to the depth the wire's jitter needs, because every late arrival leaves one more waiting the tick after. `pace: false` keeps the old behaviour so the difference is measurable, and `main.ts` is untouched: the joiner already ticked its client once per step. Measured in `simcheck` on the host's own hull as the client draws it, over a loopback with two ticks of latency, three of jitter and 5% duplicates, against the host's own per-tick motion so a real event (two hulls bouncing) is not mistaken for the wire. Applied on arrival: 319 stalls and 222 jumps in 1200 ticks. Paced: none of either; whatever was applied is still the host's world byte for byte (1195 ticks; the seat the client flies itself is predicted ahead and masked); the one coasted tick was within 0.005 units of the snapshot it stood in for; the queue settled three deep by tick 300 and stayed there; the picture never trailed the wire by more than latency plus jitter. A deterministic probe loses exactly one snapshot on a perfect wire with the host flying straight: the hull moves 5.86 units to within 0.002 of where that snapshot had it, the eight bolts in flight likewise, and the tick after is the host's world again. A client that stopped ticking for forty ticks holds sixteen and is back within depth ten ticks after it resumes. The two existing wire checks now compare against the host's world at the tick the client applied, on the ticks it applied one, which is the property they were always after (the same-iteration comparison was an accident of applying on arrival). The bad-wire check gains "the world never went back in time" and "lost snapshots were coasted through" (393 of 1200 at 30% loss). The prediction check's "trails by the latency" became a signed along-track measurement: the predicted hull flies 16 units ahead of where the host has it, the unpredicted one 17 behind. Simulation checks 548 -> 566, seven new mutations. Not here: an interpolation delay adapted to measured jitter rather than grown from it, coasting orientation (turn rate is not sent), and the host-side twin -- a peer's intents are still flown as they arrive. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
…own tick Applying moved from the wire's handler, where a throwing `Game.apply` was already caught and counted, into the client's tick, where it was not. The mutation gate found it: with respawn forced off the host match resolves, `finish` clears the roster, and the host keeps sending snapshots of nobody -- which the paced client now applied outside any catch and took the whole suite down with. Caught, counted as malformed, and the tick goes on; a check sends a one-seat snapshot to a two-seat client to say so. Also re-anchors one existing mutation to the moved replay-window line. Simulation checks 566 -> 567. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Stephen DeLorme <stephen@d.elor.me>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The first two-device test of #25/#26 connected, flew, and was jerky on the joined player while the host was smooth. The host's pairs of poses advance exactly once per tick by construction. The client's did not: it applied every snapshot the moment it arrived, and the frame then blended between the last two poses at a factor taken from the local clock, so a pair that advanced twice between frames jumped and one that had not advanced slid backwards.
What changes
createClientholds what arrives early in a queue sorted by tick; a tick that finds nothing coasts; a gap at the head of the queue is coasted once before it is applied, so a snapshot reordered past its predecessor waits for it instead of skipping it, and a lost one is a coast rather than a jump; a queue deeper thanSNAPSHOT_DEPTH(four ticks) drains two a tick, bounded atSNAPSHOT_QUEUE. The queue fills only to the depth the wire's jitter needs, because every late arrival leaves one more waiting the tick after.pace: falsekeeps the old behaviour so the difference is measurable.Game.coast(new): every hull and bolt the mirror does not fly itself, carried one tick along its last velocity, the same integrationShip.stepand the bolt pool do, so on straight flight it lands where the missing snapshot would have. Orientation is held (turn rate is not sent).main.tsis untouched: the joiner already ticked its client once per step.Evidence
Measured in
simcheckon the host's own hull as the client draws it, over a loopback with two ticks of latency, three of jitter and 5% duplicates, against the host's own per-tick motion so a real event (two hulls bouncing) is not mistaken for the wire:The queue settled three deep by tick 300 and stayed there; the picture never trailed the wire by more than latency plus jitter. A deterministic probe loses exactly one snapshot on a perfect wire with the host flying straight: the hull moves 5.86 units to within 0.002 of where that snapshot had it, the eight bolts in flight likewise, and the tick after is the host's world again. A client that stopped ticking for forty ticks holds sixteen and is back within depth ten ticks after it resumes.
The two existing wire checks now compare against the host's world at the tick the client applied, on the ticks it applied one, which is the property they were always after. The bad-wire check gains "the world never went back in time" and "lost snapshots were coasted through" (393 of 1200 at 30% loss). The prediction check's "trails by the latency" became a signed along-track measurement: the predicted hull flies 16 units ahead of where the host has it, the unpredicted one 17 behind.
check:sim548 → 567,check:balanceholds,check:mutants91 of 91 caught cleanly (+7),npm run buildclean.Not here
An interpolation delay adapted to measured jitter rather than grown from it; coasting orientation; the host-side twin (a peer's intents are still flown as they arrive). Milestone 8 (match rules / scoring / kill feed) is next, on top of this.
🤖 Generated with Claude Code