feat(physics): [cp-write] probe for ContactPlane retention spike
Spike for the next phase of indoor-walking work: confirm/refute the hypothesis that FindEnvCollisions's indoor branch rewrites the player's ContactPlane every frame instead of retaining it across frames (retail's actual behavior). The previous session shipped 6 commits on a wrong diagnosis; this probe captures the data BEFORE designing the fix. Two pieces: 1. Add PhysicsDiagnostics.ProbeContactPlaneEnabled flag, gated on ACDREAM_PROBE_CONTACT_PLANE=1 (also runtime-toggleable). Helper methods LogCpBoolWrite / LogCpPlaneWrite / LogCpCellIdWrite emit one [cp-write] line per CP/LKCP field mutation with caller (walked from the stack with file+line info) when the value actually changes. 2. Convert the 8 ContactPlane group + LastKnownContactPlane group fields on CollisionInfo from public fields to public properties with backing fields. Setters call the diagnostic helpers when the probe is on; getters/setters are inlined when the flag is off. Storage layout unchanged. No call site changes — grep confirmed no ref/out passing or sub-field writes. Build green; tests green at the existing 8-failure baseline (2 BSPStepUp, 6 MotionInterpreter — all unrelated, pre-existing). Capture command: ACDREAM_PROBE_CONTACT_PLANE=1 ACDREAM_PROBE_INDOOR_BSP=1 ACDREAM_DEVTOOLS=1 Spike-only — remove when the retention fix lands and the diagnostic value is captured in the next phase's spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7034be9294
commit
66de00d09a
2 changed files with 175 additions and 8 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.Core.Physics;
|
||||
|
||||
|
|
@ -221,4 +222,70 @@ public static class PhysicsDiagnostics
|
|||
/// </summary>
|
||||
public static bool ProbeCellCacheEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CELL_CACHE") == "1";
|
||||
|
||||
/// <summary>
|
||||
/// ContactPlane retention spike (2026-05-20). When true, every write to
|
||||
/// <c>CollisionInfo.ContactPlane{,Valid,CellId,IsWater}</c> and
|
||||
/// <c>LastKnownContactPlane{,Valid,CellId,IsWater}</c> emits one
|
||||
/// <c>[cp-write]</c> line: field, old → new value, caller method (walked
|
||||
/// from the stack), and source line. Maps the per-frame lifecycle of the
|
||||
/// contact plane to confirm/refute the hypothesis that
|
||||
/// <c>FindEnvCollisions</c> indoor branch is rewriting CP every frame
|
||||
/// instead of retaining it across frames.
|
||||
///
|
||||
/// <para>
|
||||
/// Only logs when the value actually changes (suppresses no-op writes to
|
||||
/// reduce log volume). Initial state from
|
||||
/// <c>ACDREAM_PROBE_CONTACT_PLANE=1</c>. Spike-only — remove once the fix
|
||||
/// lands and the diagnostic value is captured.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static bool ProbeContactPlaneEnabled { get; set; } =
|
||||
Environment.GetEnvironmentVariable("ACDREAM_PROBE_CONTACT_PLANE") == "1";
|
||||
|
||||
public static void LogCpBoolWrite(string field, bool oldValue, bool newValue)
|
||||
{
|
||||
var caller = GetCpCallerName();
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[cp-write] {field}: {oldValue} -> {newValue} caller={caller}"));
|
||||
}
|
||||
|
||||
public static void LogCpPlaneWrite(string field, Plane oldPlane, Plane newPlane)
|
||||
{
|
||||
var caller = GetCpCallerName();
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[cp-write] {field}: n=({oldPlane.Normal.X:F3},{oldPlane.Normal.Y:F3},{oldPlane.Normal.Z:F3}) D={oldPlane.D:F3} -> n=({newPlane.Normal.X:F3},{newPlane.Normal.Y:F3},{newPlane.Normal.Z:F3}) D={newPlane.D:F3} caller={caller}"));
|
||||
}
|
||||
|
||||
public static void LogCpCellIdWrite(string field, uint oldValue, uint newValue)
|
||||
{
|
||||
var caller = GetCpCallerName();
|
||||
Console.WriteLine(System.FormattableString.Invariant(
|
||||
$"[cp-write] {field}: 0x{oldValue:X8} -> 0x{newValue:X8} caller={caller}"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Walks the stack to identify the first frame outside <c>CollisionInfo</c>
|
||||
/// and <c>PhysicsDiagnostics</c> — that's the actual caller writing the
|
||||
/// ContactPlane field. Format: <c>TypeName.MethodName:line</c> when file
|
||||
/// info is available, else just <c>TypeName.MethodName</c>. Walked with
|
||||
/// <c>fileNeeded=true</c> only when the probe flag is on, so zero cost
|
||||
/// when off.
|
||||
/// </summary>
|
||||
private static string GetCpCallerName()
|
||||
{
|
||||
// Skip 2: this method + the LogCp*Write helper that called it.
|
||||
var st = new System.Diagnostics.StackTrace(2, fNeedFileInfo: true);
|
||||
for (int i = 0; i < st.FrameCount; i++)
|
||||
{
|
||||
var f = st.GetFrame(i);
|
||||
var m = f?.GetMethod();
|
||||
if (m is null) continue;
|
||||
var typeName = m.DeclaringType?.Name ?? "?";
|
||||
if (typeName == "CollisionInfo" || typeName == "PhysicsDiagnostics") continue;
|
||||
int line = f?.GetFileLineNumber() ?? 0;
|
||||
return line > 0 ? $"{typeName}.{m.Name}:{line}" : $"{typeName}.{m.Name}";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,15 +125,115 @@ public sealed class ObjectInfo
|
|||
/// </summary>
|
||||
public sealed class CollisionInfo
|
||||
{
|
||||
public bool ContactPlaneValid;
|
||||
public Plane ContactPlane;
|
||||
public uint ContactPlaneCellId;
|
||||
public bool ContactPlaneIsWater;
|
||||
// ContactPlane group and LastKnownContactPlane group are properties (not
|
||||
// fields) so the ContactPlane retention spike (2026-05-20) can log every
|
||||
// mutation via PhysicsDiagnostics.ProbeContactPlaneEnabled. Backing
|
||||
// fields keep storage layout identical; getters/setters are inlined by
|
||||
// the JIT when the probe flag is off. No call site needs to change —
|
||||
// `ci.ContactPlane = plane` continues to compile unchanged.
|
||||
private bool _contactPlaneValid;
|
||||
private Plane _contactPlane;
|
||||
private uint _contactPlaneCellId;
|
||||
private bool _contactPlaneIsWater;
|
||||
|
||||
public bool LastKnownContactPlaneValid;
|
||||
public Plane LastKnownContactPlane;
|
||||
public uint LastKnownContactPlaneCellId;
|
||||
public bool LastKnownContactPlaneIsWater;
|
||||
private bool _lastKnownContactPlaneValid;
|
||||
private Plane _lastKnownContactPlane;
|
||||
private uint _lastKnownContactPlaneCellId;
|
||||
private bool _lastKnownContactPlaneIsWater;
|
||||
|
||||
public bool ContactPlaneValid
|
||||
{
|
||||
get => _contactPlaneValid;
|
||||
set
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeContactPlaneEnabled && _contactPlaneValid != value)
|
||||
PhysicsDiagnostics.LogCpBoolWrite("ContactPlaneValid", _contactPlaneValid, value);
|
||||
_contactPlaneValid = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Plane ContactPlane
|
||||
{
|
||||
get => _contactPlane;
|
||||
set
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeContactPlaneEnabled && !PlaneEquals(_contactPlane, value))
|
||||
PhysicsDiagnostics.LogCpPlaneWrite("ContactPlane", _contactPlane, value);
|
||||
_contactPlane = value;
|
||||
}
|
||||
}
|
||||
|
||||
public uint ContactPlaneCellId
|
||||
{
|
||||
get => _contactPlaneCellId;
|
||||
set
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeContactPlaneEnabled && _contactPlaneCellId != value)
|
||||
PhysicsDiagnostics.LogCpCellIdWrite("ContactPlaneCellId", _contactPlaneCellId, value);
|
||||
_contactPlaneCellId = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContactPlaneIsWater
|
||||
{
|
||||
get => _contactPlaneIsWater;
|
||||
set
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeContactPlaneEnabled && _contactPlaneIsWater != value)
|
||||
PhysicsDiagnostics.LogCpBoolWrite("ContactPlaneIsWater", _contactPlaneIsWater, value);
|
||||
_contactPlaneIsWater = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool LastKnownContactPlaneValid
|
||||
{
|
||||
get => _lastKnownContactPlaneValid;
|
||||
set
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeContactPlaneEnabled && _lastKnownContactPlaneValid != value)
|
||||
PhysicsDiagnostics.LogCpBoolWrite("LastKnownContactPlaneValid", _lastKnownContactPlaneValid, value);
|
||||
_lastKnownContactPlaneValid = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Plane LastKnownContactPlane
|
||||
{
|
||||
get => _lastKnownContactPlane;
|
||||
set
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeContactPlaneEnabled && !PlaneEquals(_lastKnownContactPlane, value))
|
||||
PhysicsDiagnostics.LogCpPlaneWrite("LastKnownContactPlane", _lastKnownContactPlane, value);
|
||||
_lastKnownContactPlane = value;
|
||||
}
|
||||
}
|
||||
|
||||
public uint LastKnownContactPlaneCellId
|
||||
{
|
||||
get => _lastKnownContactPlaneCellId;
|
||||
set
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeContactPlaneEnabled && _lastKnownContactPlaneCellId != value)
|
||||
PhysicsDiagnostics.LogCpCellIdWrite("LastKnownContactPlaneCellId", _lastKnownContactPlaneCellId, value);
|
||||
_lastKnownContactPlaneCellId = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool LastKnownContactPlaneIsWater
|
||||
{
|
||||
get => _lastKnownContactPlaneIsWater;
|
||||
set
|
||||
{
|
||||
if (PhysicsDiagnostics.ProbeContactPlaneEnabled && _lastKnownContactPlaneIsWater != value)
|
||||
PhysicsDiagnostics.LogCpBoolWrite("LastKnownContactPlaneIsWater", _lastKnownContactPlaneIsWater, value);
|
||||
_lastKnownContactPlaneIsWater = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool PlaneEquals(Plane a, Plane b) =>
|
||||
a.Normal.X == b.Normal.X &&
|
||||
a.Normal.Y == b.Normal.Y &&
|
||||
a.Normal.Z == b.Normal.Z &&
|
||||
a.D == b.D;
|
||||
|
||||
public bool SlidingNormalValid;
|
||||
public Vector3 SlidingNormal; // XY only (Z zeroed)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue