docs #429 + probe: frame stalls are pack-independent (~1.7/s both arms); the pack converts them into visible player jumps (59 vs 3); prediction-snap theory dead
All checks were successful
CI / linux-portable (push) Successful in 3m30s
CI / windows-gate (push) Successful in 6m22s
CI / release (push) Successful in 2m15s

Adds the TEMPORARY PlayerPresentationProbe (ACDREAM_PROBE_PLAYER_PRESENT)
per the #429 apparatus plan and records the measured two-arm evidence in
the issue. Probe strips with the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-23 21:21:04 +02:00
parent 9deb1a28cf
commit ca4bae776c
3 changed files with 116 additions and 0 deletions

View file

@ -0,0 +1,75 @@
using System.Diagnostics;
using System.Globalization;
using System.Numerics;
namespace AcDream.App.Rendering;
/// <summary>
/// TEMPORARY #429 apparatus. One CSV row per rendered frame:
/// <c>seconds,camX,camY,camZ,playerX,playerY,playerZ</c>, where seconds is a
/// Stopwatch-derived monotonic time and player is the frame's
/// <c>PlayerViewPosition</c> — the presented local-player position driving
/// lighting/visibility this frame. Off unless
/// <c>ACDREAM_PROBE_PLAYER_PRESENT=&lt;path&gt;</c> 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.
/// </summary>
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.
}
}
}

View file

@ -450,6 +450,10 @@ internal sealed class RuntimeWorldFrameEnvironmentPreparation
private readonly HashSet<uint> _visibleCells = [];
private bool _visibleCellsValid;
/// <summary>TEMPORARY #429 — see <see cref="PlayerPresentationProbe"/>.</summary>
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);