diff --git a/docs/ISSUES.md b/docs/ISSUES.md index ee09dd1e..fd4b9ed5 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -115,6 +115,39 @@ ACE's >=800 misread), accumulating drift that the AutoPos/correction loop snaps back on its cadence. Walking (speed 1.0 both sides) and strafing accumulate little or no drift — matching the report exactly. +**MEASURED 2026-08-23 (same session) — the story inverted twice and is now +pinned by data.** A per-frame probe (`PlayerPresentationProbe`, +`ACDREAM_PROBE_PLAYER_PRESENT=`, TEMPORARY, wired in +`WorldRenderFrameBuilder`/`RuntimeWorldFrameEnvironmentPreparation`) +captured `(t, camera, presented player position)` per frame over ~50 s +of running per arm (`artifacts/owner-gate/player-present-429-packON.csv` +/ `-packoff.csv`, ~270 FPS baseline, median frame 3.7 ms): + +- Frame stalls of 15-25 ms (max ~230-260 ms) occur ~1.7/s while moving in + BOTH arms — pack ON 88, pack OFF 83 — the Atmospheric pack does NOT + cause the stalls. The uncapped run reproducing the hitch had already + killed the #235 capped-alias theory. +- The pack changes what a stall LOOKS like: with pack ON, 59 of the 88 + long frames carry a 3x-5x player-position jump (the visible hitch); + with pack OFF only 3 of 83 do — the same stall lands at a pipeline + phase where the presented position has not advanced, so the player + stays visually smooth and the owner never perceived it. +- Right after ON-arm stall clusters, normal-length frames show ~zero + player motion (catch-up artifacts). The prediction/AutoPos-correction + hypothesis is DEAD: position advances exactly proportionally to + elapsed time through the stalls; no snap-back is present in the data. + +**Two separated defects:** +1. BASE CLIENT: periodic 15-25 ms frame stalls (~1.7/s while moving). + GC exonerated (Gen0 at 6.3 s cadence, gen1/2 zero). Suspects: + streaming publication/upload bursts on movement, present-path stalls. + Instrument with a per-stall stack/phase capture, not more theories. +2. PACK FRAME GRAPH: its ordering shifts the stall's position relative + to the physics commit/presented-position sampling, converting silent + stalls into visible player jumps. Establish where the ON-arm long + frames spend their extra time (the pack's CPU stage profiler names + stages) and where the player position is sampled relative to it. + **Next probes (in order):** 1. `ACDREAM_DUMP_MOTION=1` + a temporary inbound-position log for the LOCAL guid: does ACE send position sets for the local player every diff --git a/src/AcDream.App/Rendering/PlayerPresentationProbe.cs b/src/AcDream.App/Rendering/PlayerPresentationProbe.cs new file mode 100644 index 00000000..d3a471b0 --- /dev/null +++ b/src/AcDream.App/Rendering/PlayerPresentationProbe.cs @@ -0,0 +1,75 @@ +using System.Diagnostics; +using System.Globalization; +using System.Numerics; + +namespace AcDream.App.Rendering; + +/// +/// TEMPORARY #429 apparatus. One CSV row per rendered frame: +/// seconds,camX,camY,camZ,playerX,playerY,playerZ, where seconds is a +/// Stopwatch-derived monotonic time and player is the frame's +/// PlayerViewPosition — the presented local-player position driving +/// lighting/visibility this frame. Off unless +/// ACDREAM_PROBE_PLAYER_PRESENT=<path> is set (one null check per +/// frame). Analysis: a hitch frame shows the player's per-frame delta +/// collapsing to ~0 or doubling; whether the SAME frame's dt is smooth +/// separates a presentation-phase bug (pack frame graph sampling a stale +/// snapshot) from genuine frame-pacing spikes. Strip with the #429 fix. +/// +internal sealed class PlayerPresentationProbe : IDisposable +{ + private readonly StreamWriter _writer; + private readonly long _startTimestamp = Stopwatch.GetTimestamp(); + private int _linesSinceFlush; + + private PlayerPresentationProbe(StreamWriter writer) + { + _writer = writer; + _writer.WriteLine("seconds,camX,camY,camZ,playerX,playerY,playerZ"); + } + + internal static PlayerPresentationProbe? CreateFromEnvironment() + { + string? path = Environment.GetEnvironmentVariable("ACDREAM_PROBE_PLAYER_PRESENT"); + if (string.IsNullOrWhiteSpace(path)) + return null; + + try + { + return new PlayerPresentationProbe(new StreamWriter(path, append: false)); + } + catch (Exception ex) + { + Console.Error.WriteLine( + $"[player-present] probe file '{path}' could not be opened: {ex.Message}"); + return null; + } + } + + internal void Observe(in Vector3 cameraPosition, in Vector3 playerViewPosition) + { + double seconds = (Stopwatch.GetTimestamp() - _startTimestamp) + / (double)Stopwatch.Frequency; + _writer.WriteLine(string.Create( + CultureInfo.InvariantCulture, + $"{seconds:F6},{cameraPosition.X:F4},{cameraPosition.Y:F4},{cameraPosition.Z:F4},{playerViewPosition.X:F4},{playerViewPosition.Y:F4},{playerViewPosition.Z:F4}")); + if (++_linesSinceFlush >= 240) + { + _linesSinceFlush = 0; + _writer.Flush(); + } + } + + public void Dispose() + { + try + { + _writer.Flush(); + _writer.Dispose(); + } + catch + { + // A probe must never turn teardown fallible. + } + } +} diff --git a/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs b/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs index 7fdc66ae..86be4d0a 100644 --- a/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs +++ b/src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs @@ -450,6 +450,10 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation private readonly HashSet _visibleCells = []; private bool _visibleCellsValid; + /// TEMPORARY #429 — see . + private readonly PlayerPresentationProbe? _playerPresentProbe = + PlayerPresentationProbe.CreateFromEnvironment(); + public RuntimeWorldFrameEnvironmentPreparation( RuntimeOptions options, WorldTimeService worldTime, @@ -485,6 +489,10 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation activeDayGroup, camera.Position); + // TEMPORARY #429 apparatus — one CSV row per frame; off unless + // ACDREAM_PROBE_PLAYER_PRESENT names a file. Strip with the fix. + _playerPresentProbe?.Observe(camera.Position, roots.PlayerViewPosition); + UpdateSunFromSky(foundation.Sky, roots.PlayerInsideCell); _lighting.UpdateViewerLight(roots.PlayerViewPosition); _lighting.Tick(camera.Position);