Skip to content

Single-vehicle sync improvements#220

Draft
spectrachrome wants to merge 36 commits into
TheHellBox:masterfrom
spectrachrome:multi-cluster-sync
Draft

Single-vehicle sync improvements#220
spectrachrome wants to merge 36 commits into
TheHellBox:masterfrom
spectrachrome:multi-cluster-sync

Conversation

@spectrachrome
Copy link
Copy Markdown

@spectrachrome spectrachrome commented Apr 18, 2026

Model

Cluster topology

A cluster is a collection of $\displaystyle N$ rigid bodies. A truck with trailer is two bodies. An articulated bus is two bodies. Each body $\displaystyle B_i$ has a complete world-space transform that is synchronized independently.

State representation

The full cluster state at time $\displaystyle t$ is a set of body transforms:

$$\Large\mathbf{S}(t) = { \underbrace{(\mathbf{x}_i, \mathbf{q}_i, \mathbf{v}_i, \boldsymbol{\omega}_i)}_{\text{body } i \text{ transform}} }_{i=0}^{N-1}$$

Where for each body $\displaystyle B_i$:

  • $\displaystyle \mathbf{x}_i \in \mathbb{R}^3$ is the body's position in world coordinates.
  • $\displaystyle \mathbf{q}_i \in \mathbb{H}$ is the body's orientation as a unit quaternion.
  • $\displaystyle \mathbf{v}_i \in \mathbb{R}^3$ is the body's linear velocity.
  • $\displaystyle \boldsymbol{\omega}_i \in \mathbb{R}^3$ is the body's angular velocity.

Key principle: All body transforms are transmitted explicitly. Joint articulation is implicit in the relative transforms between bodies — joint angles are not part of wire state:

$$\Large\theta_{\text{joint}} = \text{angle}(\mathbf{q}_0^{-1} \cdot \mathbf{q}_1)$$

The wire carries only the outcome of the physics simulation, not the internal joint state.

Authority distribution

Physics ownership

The cluster owner simulates physics for all bodies using BeamNG's native couplers and solver. Non-owner peers perform direct state replay: they receive $\mathbf{S}(t)$ snapshots and set body transforms directly. Non-owner peers do not integrate forces or compute forward kinematics.

Formally, for any peer $\displaystyle P$ and any body $\displaystyle B_i$ in a cluster owned by $\displaystyle O \neq P$:

$$\Large T_i^P(t) = T_i^O(t - \text{latency})$$

All body transforms come from the wire, never from local simulation.

Invariant: replay peers execute direct state application only, no dynamics, no forward kinematics.

Rear steering

Bidirectional physics coupling occurs within the owner's BeamNG solver. The wire carries only the resulting body states. Rear-steer input is control state, not cluster state.

Invariant: control inputs and cluster state are separate message types. Steering angles are consumed by the owner to produce $\displaystyle \mathbf{S}(t+\Delta t)$. Replay peers receive the result — rear-steer angle need not be transmitted.

Invariants

If these four hold, drift is impossible by construction:

  1. Single integrator. For each cluster, exactly one peer (the owner) integrates forces through BeamNG's native solver. All others replay.

  2. Explicit body states. Wire state is $\displaystyle {(\mathbf{x}_i, \mathbf{q}_i, \mathbf{v}_i, \boldsymbol{\omega}i)}{i=0}^{N-1}$ — all body transforms explicit. No derived poses, no joint DOFs.

  3. Direct replay. Replay peers set body transforms directly from wire data. No forward kinematics, no joint reconstruction, no independent physics.

  4. Control/state separation. Control inputs flow peer → owner. State flows owner → peers. These are different message types with different guarantees.

The only drift sources are numerical (float precision) and latency (peer sees old state). Both are bounded by how recently the owner's last $\displaystyle \mathbf{S}(t)$ arrived.

Implementation Scope

This PR implements:

  • Wire schema for $\displaystyle \mathbf{S}(t)$ with explicit per-body transforms (position, rotation, linear velocity, angular velocity)
  • component_id field to distinguish bodies within a cluster group (reserved in Phase 1b, activated in Phase 2)
  • Replay peer path using direct state application (no independent trailer physics, no forward kinematics)
  • Single-owner authority model (cross-owner handover deferred)
  • Measurement primitives (divergence ring buffer, p50/p95/p99 statistics)

Out of Scope

  • Cross-owner authority handover. Single owner per cluster assumed. Handover protocol (epoch system, two-phase commit) is deferred.
  • Emergent contact clusters. Contact graph and automatic cluster formation (pile-ups, tow ropes, cars on trailers) specified separately.
  • Split control. Co-op driving (separate players controlling front/rear) supported by model but not implemented.
  • Damage sync. Broken beams and deformation state tracked separately.

Review Guidance

Wire schema

Verify all body transforms are explicit with no joint DOFs. Confirm component_id is reserved for cluster extension.

Replay path

Confirm direct state application is the sole source of body poses. Verify no residual forward kinematics or independent trailer physics simulation.

Authority boundary

Confirm owner simulates full cluster through BeamNG's native solver before broadcasting $\displaystyle \mathbf{S}(t)$. Verify control input handling is separated from state transmission.

Testing

  • Single truck + trailer: no drift over 5 minutes (all observers)
  • Hitch/unhitch: no pose snap at coupling or decoupling
  • Articulated bus (rear steering): hinge angle p95 < 0.5° (owner vs observers)
  • Packet loss 5-15%: dead-reckoning holds, no teleport on snapshot arrival
  • Reconnection: mid-session join receives current cluster state

Known Risks

Two places where the model meets BeamNG's API and might need adjustment:

  1. applyClusterLinearAngularAccel compatibility. The foundation assumes the owner can integrate the cluster through its solver without the API imposing sync assumptions that fight the "owner simulates everything" invariant. This needs verification on Phase 1/2 work.

  2. Coupling handshake latency. The two-phase protocol adds at least one round-trip between detection and commitment. If this is perceptible to the player pulling up to a hitch, we may need speculative commit with server rollback rather than strict two-phase commit. That's a UX tradeoff to evaluate later.

References

  • Full model derivation: ClusterSyncFoundation.md
  • Implementation roadmap: ClusterSyncRoadmap.md
  • Debugger toolchain (for testing): HotReloadingDebuggerRoadmap.md

- Add pose divergence ring buffer tracking position/orientation delta
- Implement hinge angle measurement for articulated vehicles
- Add live statistics computation with p50/p95/p99 percentiles
- Provide CSV export for external analysis (Python, MATLAB)
- Include 10 unit tests validating quaternion math and buffer operations
- Update ClusterSyncRoadmap.md marking Phase 1a as complete

All exit criteria met: tooling produces sensible numbers, CSV export works,
no console spam, 30s buffer at 60Hz implemented and tested.
- Register mp_measurement_* commands in BeamNG console
- Provide CLI interface for pose divergence and hinge angle export
- Commands: enable, disable, export_pose, export_hinge, export_report, stats, clear, status
- Integration point for Phase 1a measurement primitives
… tracking

- Replace joint-angle tracking with per-body root state sync
- Remove forward kinematics from replay path (direct state setting)
- Update state representation: S(t) = {(x_i, q_i, v_i, ω_i)} for all bodies
- Hitch angles now implicit in relative transforms, not wire state
- Add authority handover protocol for cross-owner coupling
- Add multi-body sync atomicity risk to known issues
- Fix MathJax delimiter errors (remove \left/\right commands)
- Clean up duplicate clauses and simplify equations
- Add explicit O (owner) and P (peer) notation definitions

Model revision ensures crashes and deformation match authority exactly
by syncing the outcome of BeamNG physics, not re-simulating joints.
… blending

Complete implementation of dead-reckoning prediction and smooth state replay
for multiplayer vehicle synchronization in ForkedKISS.

Rust implementation (shared crate):
- New sync module with prediction.rs and replay.rs
- Dead-reckoning: position extrapolation x(t+Δt) = x(t) + v(t)·Δt
- Quaternion integration: q(t+Δt) = q(t) ⊗ exp(ω·Δt/2)
- Spherical linear interpolation (slerp) for rotation blending
- Default 150ms blend duration for smooth snapshot transitions
- component_id field added to VehicleUpdate (Phase 2 ready)
- 10 new unit tests, all passing (20 total in shared crate)

Lua implementation (BeamNG side):
- New kiss_sync.lua module with prediction and blending logic
- kiss_transforms.lua updated to use direct state setting
- Replaces old force-based physics interpolation approach
- Direct position/rotation setting eliminates drift and oscillation
- Maintains coherence with ClusterSyncFoundation.md invariant:
  T_i^P(t) = T_i^wire(t)

Integration:
- kissmp-server broadcasts VehicleUpdate with component_id field
- vehiclemanager.lua → kisstransform.lua → kiss_transforms.lua flow intact
- No breaking changes to network protocol (component_id = vehicle_id in Phase 1)

Testing:
- All 20 Rust unit tests passing (prediction math, blending, multi-body)
- Lua syntax validated with luac
- Ready for live validation testing with measurement tools from Phase 1a

Exit criteria status:
- Wire format complete ✓
- Prediction between snapshots ✓
- Blending on arrival (no teleports) ✓
- Multi-body ready (component_id reserved) ✓
- Live validation: ready for testing

References:
- ClusterSyncRoadmap.md#L147-169 (Phase 1b specification)
- ClusterSyncFoundation.md (mathematical model)
- Phase1b_Implementation.md (detailed implementation guide)
The sync and measurement modules were never used by production code.
Quaternion math belongs on the client (Lua) side, not in shared types.
Server correctly treats rotations as opaque [f32; 4] data only.
@Vlad118
Copy link
Copy Markdown
Collaborator

Vlad118 commented Apr 18, 2026

I wanted to let you know our current thoughts on your work:

Looking at some of your changes we are mainly concerned that it strays away from the relative simplicity and maintainability of the current codebase (i.e. KISS principle). In the current state of these mass changes, you still mention limitations like drift/error accumulation and potentially needing vehicle-specific presets. Also, you mention some other complex "Out of Scope" aspects, would you also be planning on having them implemented too? As this continues to raise the total complexity further.

As I said before, if you can break it down into modular changes it would be nicer to review, test and consider. You'd probably have a better idea on how to split this. Next, we can look into merging any stable and simple changes first, proven and consistent bugfixes. Cluster-sync and other crazy stuff should be kept on a separate branch. I'm wondering if it would be sensible to perhaps have an experimental toggle for this model, even, if it proves to be reliable eventually. Ideally this should be kept up to sync with the master branch. As you said correctly, this may be a bit of a faff but hopefully we can agree on a way to make it easier for you, especially as we are not currently working on mass changes to this logic (apart from refactoring from @DaddelZeit, which is sensible given that the codebase is years old). Ultimately, the rest of us are currently in a state of maintenance, bugfixing and gradual improvement.

The other concerns are impact on performance and updates to the game itself. The performance concern is both for local resources and effect of internet latency (which this game suffers from greatly, regardless of how great the sync implementation is). I'm not sure myself how realistic the latter is, so please correct me if I'm wrong, but your current changes seem very sensitive to parameters and increase reliance on game engine events which, when updated or tweaked, I assume would result in more maintenance being required from us.

I also notice a lot of AI agentic work being used. While it's powerful, it does seem to be doing some fairly large rewrites and bloating of the code which is daunting for us reviewers.

We appreciate this pretty significant effort, but hopefully you can understand our points as we're hoping to coordinate and experiment with these changes. And hopefully this is not read as some wacky corporate email as I wanted to be logical and polite :))

Also relevant to #188.

@spectrachrome
Copy link
Copy Markdown
Author

spectrachrome commented Apr 18, 2026

Hi there! Thanks for the feedback.
The larger PR will be closed anyway.

drift/error accumulation and potentially needing vehicle-specific presets

This is the inherent limitation of the previous vehicle-syncing approach you've seen in all the videos I've posted with trailers. Different vehicle configurations, say, an eSBR 500 with a small tilt bed has its center point somewhere else than just the car, or a capsule bus.

You can tune some parameters in the vehicle clusters (or vehicle groups), and get it working quite well for an articulated bus, and it will pretty much fix the drift there - however, the car and trailer drift will now diverge from the true state again.

You just simply cannot fix this with PD/PID sync.
Cluster sync with velocities applied to all nodes is pretty much the only way to avoid chasing targets you'll never catch.


If this even in a modularized state, for now just a single car, etc... is still too much (doesn't fit your definition of simple), I would drop the work on trailers.

@spectrachrome
Copy link
Copy Markdown
Author

spectrachrome commented Apr 18, 2026

your current changes seem very sensitive to parameters

The entire point of this rather dry mathematical model is to avoid that. It's a single branch of logic for everything. The opposite of the other PR that I will close.

Since you seem fine with going forward in a stepped and modular fashion ...

How about we simply start with cars turning correctly and closing a lot of the latency drift gap, thus position offset.

As far as I have tested master, the car position gets up to a meter off, diverging with acceleration and converging with deceleration, also, the car turns way too late, not because of angular prediction, but because the car turns around its centerpoint - and not where it's actually articulated (the front steered wheels).

This would be simplest case of node cluster sync (single vehicle, single cluster), pose hopefully no large risk of regression and would make the rendering of other cars visibly smoother and more realistic.

In principle this should even work for an articulated bus as all of the simulated positions and velocities are sent over the wire exactly as simulated on the owner's side.

What do you think?
Have a wonderful day

@spectrachrome
Copy link
Copy Markdown
Author

The mathematical model in the PR description is mostly about the node velocity-sync that allows car movement to replay exactly for others how it did on the owner's instance.

Also, only one BeamNG instance should ever have the authority to simulate a cluster where ownership is fragmented (truck may be Person A and trailer Person B) but that's a corner case.

@spectrachrome spectrachrome changed the title Pose-based multi-cluster sync Pose-based node cluster sync Apr 18, 2026
Add direct state replay for vehicle deformation via node positions.

Rust:
- Add Deformation struct with node_positions field
- Add deformation: Option<Deformation> to VehicleUpdate

Lua:
- Rewrite kiss_nodes.lua: capture_nodes() and apply_nodes() API
- Integrate deformation into vehiclemanager.send_vehicle_update()
- Apply nodes in kiss_transforms.set_target_transform()
- Pass deformation through kisstransform.update_vehicle_transform()

Docs:
- Update ClusterSyncRoadmap.md Phase 2 for explicit per-body transforms

Architecture:
- Authority captures node positions every tick
- Receivers apply positions directly (no velocity prediction)
- BeamNG physics handles intermediate dynamics locally
- Handles all vehicle types uniformly

Bandwidth: ~144 KB/s per vehicle at 60Hz (200 nodes)
Future: quantization + delta for 10-15x reduction
@spectrachrome spectrachrome changed the title Pose-based node cluster sync Vehicle-agnostic single-cluster sync Apr 18, 2026
Server relays deformation data from Lua clients, doesn't generate it.
@Dummiesman
Copy link
Copy Markdown
Collaborator

I think the term "cluster" here is a bit confusing because in BeamNG the term cluster refers to a part of the node/beam structure

@spectrachrome spectrachrome changed the title Vehicle-agnostic single-cluster sync Vehicle-agnostic single-cluster sync with per-node velocity matching Apr 19, 2026
The old force-match reconstructed per-node velocities from (v_COM, ω) on the
receiver, which assumes a single rigid body. That assumption breaks on every
BeamNG vehicle — wheels spin around hubs, rotors around masts, articulated
buses have multiple bodies — so the remote's nodes got pushed toward targets
inconsistent with the jbeam's actual state. Symptoms stacked: systemic
speed deficit, yaw-coupled heading drift, try_rude teleports, at-speed curve
crashes.

Replace the estimator with direct replay of per-node position and velocity.
The wire now carries both for every node; the receiver applies position via
setNodePosition and velocity via a one-physics-step impulse. No rigid-body
assumption, no COM/ref frame concerns, no sign conventions to get wrong. Every
DOF the authority has — chassis flex, wheel spin, rotor rpm, suspension, panel
deformation, articulation — is implicitly synced because it's the same node
state.

Wire format changes:
- Deformation -> ClusterNodes; gains node_velocities alongside node_positions
- VehicleUpdate.deformation -> VehicleUpdate.cluster_nodes

Server (previously dropped deformation on receive and relayed None):
- Vehicle stores cluster_nodes
- Incoming VehicleUpdate writes to it
- Tick relay forwards it to other clients

Sender:
- kiss_nodes.capture_nodes returns (positions, velocities)
- kiss_vehicle.update_transform_info captures both, pushes via GE bridge
- vehiclemanager assembles cluster_nodes on the outgoing packet

Receiver:
- kisstransform pipes cluster_nodes to the vehicle
- kiss_transforms.set_target_transform applies via kiss_nodes.apply_nodes
- kiss_transforms.update no longer calls the removed force-match formula

Removed:
- kiss_vehicle.apply_linear_velocity_ang_torque
- The COM-offset cache and sign-flip fixes scaffolded around it
Server rejected incoming packets with "invalid type: map, expected a sequence"
because Lua's capture_nodes returns tables keyed by node CID, which jsonEncode
serializes as JSON objects. The Rust side expected a JSON array via
Vec<[f32; 3]>. Switch node_positions and node_velocities to HashMap<u32, [f32; 3]>
so the wire shape matches the Lua table shape directly.

Also swap obj:getNodeVelocity -> obj:getNodeVelocityVector; current BeamNG
expects a second argument on getNodeVelocity (reference node), which we don't
want for world-frame velocity.
The receive path is JSON end-to-end (client -> server -> bridge -> client), and
JSON object keys are always strings. jsonDecode surfaces the cluster_nodes
table with string keys like "0", "12", which BeamNG's setNodePosition /
getNodeVelocityVector / applyForceVector reject because they expect numeric
CIDs. Wrap with tonumber before calling into the C++ bindings.
The previous design carried per-node state inside VehicleUpdate, which made
packets vastly exceed the QUIC datagram MTU (~1200 bytes) on anything bigger
than a Pigeon. send_datagram errored, killing the send task, and ping climbed
to the second range as side effects piled up.

Split the message model:

- VehicleUpdate now carries only cluster-level state (transform, electrics,
  gearbox, ids). Small and MTU-fit.
- ClusterNodesFragment is a new message type carrying a subset of the per-node
  state for one tick, with vehicle_id + tick_id + fragment_index + total_fragments.
  Each fragment is independently applicable on the receiver; dropped fragments
  just mean those nodes don't update that tick.

Server passes fragments through as they arrive (no storage, no reassembly) on
the unreliable channel. VehicleUpdate keeps its tick-based relay for cluster
state.

Receiver tracks highest-applied tick_id per vehicle (wrap-aware comparison),
drops older fragments, and queues kiss_nodes.apply_nodes for fresh ones.

Sender emits up to CLUSTER_NODES_PER_FRAGMENT (30) nodes per fragment — sized
so a fragment fits one datagram even before quantization.

Note: the bridge also deserializes the ServerCommand enum, so it needs a rebuild
against the new shared crate to route the new ClusterNodesFragment variant.
Unknown variants are silently dropped by the existing if-let-Ok pattern.
…gram size limitations

VehicleUpdate now carries per-node position + velocity for every node, which
for anything larger than a Pigeon exceeds the QUIC datagram size cap (~1200
bytes). send_datagram returned errors, killed the drive_send task via the
?-propagation, and ping climbed as pongs stopped flowing.

Route VehicleUpdate on the ordered stream instead. Streams have no size cap,
so oversize packets go through intact. Tradeoff: head-of-line blocking under
packet loss (packet N+1 waits for N's retransmit before delivery). Acceptable
while we figure out bandwidth reduction — correctness first, optimize second.
…o i16

Reduces per-tick cluster_nodes payload from ~70 KB to ~25 KB for a 1000-node
vehicle during normal driving, which pulls it under the CHUNK_SIZE threshold
and eliminates the chunking that was breaking big-vehicle sync.

Two changes:

1. Positions are transmitted as body-frame offsets from rest pose, not world
   positions. Chassis nodes stay within submm of rest while driving, so their
   offsets quantize to zero and get omitted from the map entirely. Only nodes
   with meaningful deformation or independent motion (wheels, suspension flex,
   panels under load) appear in node_positions. Sender subtracts cached
   rest-body-pose; receiver reconstructs rest + offset and rotates back to
   world via current body rotation.

2. Wire format is i16 for both positions and velocities:
   - Positions: mm precision (scale 1000), range ±32.767 m
   - Velocities: cm/s precision (scale 100), range ±327.67 m/s
   Both comfortably cover any realistic value while halving JSON byte cost
   versus the previous f32 representation.

Rest-pose cache built lazily on first capture/apply from v.data.nodes. The
receiver iterates all nodes regardless of whether they're in the received map;
omitted = at rest. Velocities stay per-node as before (chassis nodes have
nonzero velocity while driving, so skipping isn't beneficial there without
reintroducing the body-twist reconstruction we avoid).

No change to how kiss_transforms / kisstransform / server code routes the
messages — only the ClusterNodes wire shape and the kiss_nodes
encode/decode change.
…ting

Rebuilds Layer 2 sync as per-node DEVIATIONS from the rigid-body prediction
Layer 1 (Transform) already describes. Orthogonal by construction — receiver
can't double-count cluster motion because cluster motion was factored out at
capture time.

Capture: each node transmits body-frame pos_deviation from jbeam rest and
world-frame vel_deviation from rigid prediction. Sender-side epsilon
thresholds skip entries below noise. Chassis nodes on a cruising vehicle
deviate zero → omitted → bandwidth drops to only the nodes actually doing
something non-rigid (wheels, suspension, deformation).

Apply has three modes:
  - Initial-sync: first packet or |Δp|>2m → hard-set via setNodePosition with
    no competing applyForceVector on same tick. Avoids solver conflict.
  - Steady-state blended correction: impulse toward v_desired = v_target +
    POSITION_PULL_GAIN · Δp, with two receiver-side gates (see below).
  - Large-delta re-init: authority teleport, long disconnect, severe desync
    self-heal by re-entering initial-sync.

Two receiver-side gates make sync agnostic across vehicles and props:
  - Gate 1 (dead-band): skip applyForceVector entirely when both |Δp| and
    |Δv| are within tolerance of target. Stationary rigid props have every
    node in this regime → zero impulses → no ground-contact fight → no
    flyaway. Moving parts (spinning wheels, active suspension, deformation)
    have Δv outside the band and get corrected as before.
  - Gate 2 (structural force cap): clamp |Δv| per tick to MAX_DELTA_V_PER_TICK.
    No single bad packet / pathological reconstruction can detonate the jbeam.

Lever-arm convention fix: reconstruct rigid_vel using the node's current
position as the lever arm, matching the sender's capture convention. Using
p_target here produces an ω × Δp bias that was the cause of the earlier
~20cm stationary-Pigeon helmet bob.

Owner-choppiness fix already present: only owned vehicles run the per-node
capture workload via the we_own_this_vehicle flag threaded through
update_transform_info — remote vehicles skip the ~1000 getNode calls per tick.

Live-tunable via new imgui Tuning tab: position_scale, velocity_scale,
position/velocity epsilon (sender-side), position_pull_gain,
position/velocity dead-band, max Δv per tick (receiver-side). All persist
via kissconfig save/load and push to every vehicle on slider change and on
spawn.

No Rust wire format changes — only updated doc comments.
@spectrachrome spectrachrome changed the title Vehicle-agnostic single-cluster sync with per-node velocity matching Vehicle-agnostic single-cluster sync with selective node-geometry velocity matching Apr 24, 2026
@spectrachrome spectrachrome changed the title Vehicle-agnostic single-cluster sync with selective node-geometry velocity matching Single-vehicle sync improvements Apr 24, 2026
@spectrachrome
Copy link
Copy Markdown
Author

spectrachrome commented Apr 24, 2026

It seems the only way to preserve both positions and velocities in tandem for a soft-body physics multiplayer mod like this is a trajectory sync with a specific set of selected actuating nodes. More specifically, a node triangle near the center of the vehicle chassis, and below 10 outer points as lever arms if necessary as the actuation of the center triangle is relatively weak. So-called "support nodes" along with a "support gain".

Already simulates bus movement at the most oscillation-sensitive seat of an articulated bus (the back) quite realistically.

https://www.youtube.com/watch?v=7zzush2qU4E

I'm currently still working on readying and testing the new trajectory sync approach.

  • velocity for immediate motion
  • tangent for heading
  • normal for cross-track
  • weak tangent assist for phase
  • support only for vertical/tilt

Replace layered per-node / Layer1 cluster sync with a single motion-first
path: sender publishes COG pose+twist plus send_timer/ping_ms/send_dt;
receiver runs second-order dead-reckoning in kiss_sync and a PD correction
loop in kiss_transforms with overshoot dampening. SessionTuningUpdate and
the tuning sliders are removed; cluster_nodes payload kept only for
optional deformation.
Add optional acceleration / angular_acceleration to Transform so receivers
can skip the (v_new - v_prev)/remote_dt path that amplifies sample noise by
~1/remote_dt. Sender lowpasses its own derived COG-frame acceleration at
30 Hz and ships it on the wire; receivers prefer it over their own
differential, falling back to the legacy path when absent.

Adds Tuning tab sliders for the receiver-side vel / accel smoothing rates
(local-only, per-client) and renames acc/racc shorthands to linear_accel /
angular_accel for readability.

Also includes a short rate-limited [bob] diagnostic print in kiss_transforms
left in for the next test session.
…ccel

The receiver-side REMOTE_ACCEL_SMOOTH_RATE lowpass on linear/angular accel
was added to suppress the (v_new - v_prev)/remote_dt amplifier on the
legacy fallback path. With the sender now shipping pre-smoothed
acceleration, the receiver lerp stacked a second ~167ms time constant on
top of the sender's ~33ms, leaving smooth_linear_accel non-zero long after
the sender had settled and producing forward position drift through the
0.5*a*t^2 prediction term. Use the raw sender value (or one-shot legacy
differential) directly. Velocity lerp stays since velocity still arrives
unfiltered from the wire's perspective.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants