Files
Commonwealth-Online-Public/docs/phase2-interpolation-debugging.md
T
andrew a11c7d4475 Add interpolation system and integrate into proxy
Introduce a waypoint-based interpolation system and integrate it into the proxy actor controller. Added F4T::InterpolationSystem (header + implementation) with TimePoint, InterpolationComponent, Lerp helpers and Update() logic. Integrates per-slot InterpolationComponent into F4TProxyActorController, switches to kinematic SetPosition-based smoothing (per-frame Lerp with snap thresholds) and removes character-controller velocity injection to prevent drift; also clears waypoints on hard snaps and uses steady_clock for timing. Added a descriptor-based bulk animation write (ApplyDesiredStateToGraphDescriptorBased) in ProxyAnimationSync for efficient graph variable updates with failure logging. Also included testing and debugging docs (phase2-interpolation-testing.md and phase2-interpolation-debugging.md) and flipped some debug flags to false.
2026-06-03 22:46:57 +12:00

7.2 KiB

Phase 2 Interpolation Debugging Guide

Problem: "Still Jerky, Not Smooth Movement"

This guide will help diagnose why interpolation isn't producing smooth movement.


Quick Diagnostics

1. Check If Interpolation is Even Being Called

Log signature to find:

[Local Player ID: X] Phase 2 interpolation: remote player Y, actor=ZZZZZZZZ, smooth blending active.

If this log is NOT appearing:

  • Interpolation is not being triggered
  • Check: Is distanceDrift > 0.01F? (Remote player must be moving)
  • Check: Is a_shouldSnap == false? (Not a teleport/snap movement)
  • Check: Does a_optSlot exist? (Must be a runtime proxy, not fallback)

2. Verify Update Rate is Sufficient

What's needed for smooth interpolation:

  • Updates must arrive frequently enough to queue waypoints
  • Too infrequent updates = large gaps between waypoints = large lerps = still jerky

Check fake client update rate:

# In fake_client.py, look for the movement loop
# It should be sending updates at least 10-30 Hz (10-30 per second)

Root Causes of Jerkiness

Root Cause 1: Not Enough Waypoints

Symptom: Movement is still teleport-like, not smooth Reason: Interpolation needs AT LEAST 2 waypoints

Diagram:

Bad (jerky):
Frame 1:  ╔══ Position A
Frame 2:  ║   ╔══ Position B (teleport)
Frame 3:  ║   ║   ╔══ Position C (teleport)

Good (smooth):
Frame 1:  ╔══════════════════╗ WayPoint 1 (A) queued
Frame 2:  ║ Interp between   ║ WayPoint 2 (B) queued, lerp A→B at 0.5
          ║ A and B at 25%   ║
Frame 3:  ║ Interp between   ║ WayPoint 3 (C) queued, lerp B→C at 0.5
          ║ B and C at 50%   ║

Diagnosis:

  • Add extra logging to ApplyRuntimeProxyTransform():
LogInfoWithLocalPlayerPrefix(std::format(
    "Waypoint queue size: {}, distanceDrift: {:.2f}, interpolating: {}",
    a_optSlot->interpolationComponent.TimePoints.size(),
    distanceDrift,
    interpResult.IsInterpolating));

Root Cause 2: Update Arrival Rate Too Slow

Symptom: Movement jumps happen, then freeze, then jump again Reason: Remote player updates arriving slower than game frame rate

Example:

Game FPS: 60 (16ms per frame)
Update Rate: 10 Hz (100ms per update)

Result: 
Frame 1-6: (100ms of frames) Interpolate slowly
Frame 7:   NEW UPDATE arrives, big jump
Frame 8-13: Interpolate again

Fix: Increase fake client update frequency

# In fake_client.py, change sleep duration:
time.sleep(0.033)  # 30 Hz instead of slower

Root Cause 3: Alpha Calculation Wrong

Symptom: Movement speed changes unexpectedly, or stalls Reason: Frame counter might not be incrementing correctly

Check the alpha calculation:

// In InterpolationSystem::Update()
float alpha = static_cast<float>(tickElapsed) / static_cast<float>(tickDelta);

// If tickDelta is 0 or 1, alpha calculation breaks
// tickDelta should be the FRAME DIFFERENCE between waypoints (e.g., 30-60)

Root Cause 4: Waypoint Queue Pruning Too Aggressive

Symptom: Interpolation logs appear but movement still jerky Reason: Queue size limited to 3, old waypoints popped too early

Current code:

while (a_optSlot->interpolationComponent.TimePoints.size() > 3) {
    a_optSlot->interpolationComponent.TimePoints.pop_front();
}

Try increasing to 5-10 waypoints for more buffer.


Testing Strategy

Step 1: Add Detailed Logging

Edit F4TProxyActorController.cpp in ApplyRuntimeProxyTransform():

if (a_optSlot && !a_shouldSnap && distanceDrift > 0.01F) {
    static uint64_t frameCounter = 0;
    frameCounter++;
    
    // ADD THIS DEBUG LOG:
    if (frameCounter % 10 == 0) {  // Log every 10th frame to avoid spam
        LogInfoWithLocalPlayerPrefix(std::format(
            "Phase2-DEBUG: frame={}, distanceDrift={:.2f}, queueSize={}, interpolating={}",
            frameCounter,
            distanceDrift,
            a_optSlot->interpolationComponent.TimePoints.size(),
            /* will know after Update */
            false));
    }
    
    // ... rest of code
}

Step 2: Monitor Log Output

After recompiling, run fake client and watch log:

# Terminal: Watch logs in real-time
Get-Content "c:\Users\User\Documents\My Games\Fallout4\F4SE\Fallout4Together.log" -Wait -Tail 50 | Select-String "Phase2-DEBUG"

Expected pattern (good interpolation):

Phase2-DEBUG: frame=10, distanceDrift=345.62, queueSize=2, interpolating=true
Phase2-DEBUG: frame=20, distanceDrift=298.45, queueSize=2, interpolating=true
Phase2-DEBUG: frame=30, distanceDrift=251.23, queueSize=2, interpolating=true  ← Smooth decrease

Bad pattern (not working):

Phase2-DEBUG: frame=10, distanceDrift=345.62, queueSize=1, interpolating=false
Phase2-DEBUG: frame=20, distanceDrift=1000.00, queueSize=1, interpolating=false  ← Jumps!
Phase2-DEBUG: frame=30, distanceDrift=500.00, queueSize=1, interpolating=false

Step 3: Check Fake Client Update Rate

Add logging to fake_client.py:

last_update_time = time.time()
while True:
    # ... send update ...
    now = time.time()
    update_interval = now - last_update_time
    if update_interval > 0.05:  # Log if >50ms between updates
        print(f"WARNING: Long update interval: {update_interval*1000:.1f}ms")
    last_update_time = now
    time.sleep(0.033)  # ~30 Hz

Comprehensive Fix Checklist

  • Verify waypoints are queuing: Add frame counter debug log
  • Confirm queue size > 1: Should see "queueSize=2" in logs during movement
  • Check update frequency: Fake client sending at 20-30 Hz minimum
  • Verify distanceDrift threshold: Is distanceDrift > 0.01F?
  • Test with moving fake client: Stationary player won't trigger interpolation
  • Increase waypoint buffer: Try queueSize > 5 instead of 3
  • Monitor alpha calculation: Should go 0.0 → 1.0 as player moves between waypoints

Quick Fix: Increase Update Frequency

Easiest fix: Make fake client send updates more frequently

Edit fake_client.py:

# Find the main loop:
while True:
    # ... code ...
    time.sleep(0.033)  # Change from 0.1 or higher to 0.033 (30 Hz)

Or increase queue size in ApplyRuntimeProxyTransform():

// Change from:
while (a_optSlot->interpolationComponent.TimePoints.size() > 3) {
// To:
while (a_optSlot->interpolationComponent.TimePoints.size() > 10) {

If Still Jerky After All This

The issue might be architectural: Maybe F4 doesn't support sub-frame smooth movement, and we need to adjust strategy.

Next options:

  1. Reduce frame times via velocity injection: Make proxies move via physics instead of SetPosition
  2. Use different lerp approach: Accumulate delta over frames instead of tick-based
  3. Accept some jerkiness: Design animations to hide it (blending animations during transitions)

Need More Help?

Post the following in your logs:

  1. Output of "Phase2-DEBUG" log lines (10 consecutive)
  2. Fake client update interval timing
  3. FPS you're getting in-game
  4. Distance between waypoints in the logs

This will help diagnose the exact cause.