acdream/tests/AcDream.App.Tests/Rendering/Issue181CameraParkStabilityTests.cs
Erik 864e4d9e75 diag #181: root-cause chain complete - flapping cell 0x0181, wall-press excitation, and the REAL defect: camera-flood-scoped light application
Issue181VisFlapReplayTests: headless flood replay at the live parked pose names cell 0x8A020181 - +-0.5mm eye perturbations flip its admission (at yaw15/pitch-20 for every direction). Issue181CameraParkStabilityTests: the camera loop parks BIT-EXACT with static inputs (0.00um over 2000 frames) - the live ~1mm/frame wander is the wall-press equilibrium (sought steps a*gap ~4mm into the wall per frame, the clip lands within adjust_to_plane's parametric 0.02 window; player physics bit-frozen per [resolve]). That wobble is retail-class. The defect is OURS: the A7 adaptation scopes light application to the camera flood's per-frame admission, so a sliver cell flapping flips whole lit regions; retail computes light reach once at registration (Render::add_static_light -> CObjCell::add_lights), camera-independent. Fix direction: registration-time per-light reach sets via the portal graph (pseudocode docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md). ISSUES #181 updated with the full chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:04:34 +02:00

100 lines
4.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.Core.Rendering;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #181 excitation probe — the live parked camera wanders ~0.9 mm/frame
/// (launch-176-leakfix.log [flap-sweep]: 19,889 distinct sought values in 20k
/// parked sweeps), which keeps the portal flood's knife-edge admissions
/// flapping. Retail's parked viewer is a bit-exact fixed point (UpdateCamera
/// dead-band `return viewer`).
///
/// This test drives RetailChaseCamera.Update with BIT-IDENTICAL inputs at the
/// live frame rate: if the camera parks bit-stable here, the live wobble comes
/// from its INPUTS (player position/yaw jitter out of GameWindow); if it
/// wobbles here, the camera loop itself fails to reach the fixed point.
/// </summary>
public class Issue181CameraParkStabilityTests
{
private readonly ITestOutputHelper _out;
public Issue181CameraParkStabilityTests(ITestOutputHelper output) => _out = output;
private sealed class PassthroughProbe : ICameraCollisionProbe
{
public CameraSweepResult SweepEye(Vector3 pivot, Vector3 desiredEye, uint cellId, uint selfEntityId, Vector3 playerPos)
=> new(desiredEye, cellId);
}
[Fact]
public void ParkedCamera_StaticInputs_ReachesBitStableFixedPoint()
{
bool savedAlign = CameraDiagnostics.AlignToSlope;
bool savedColl = CameraDiagnostics.CollideCamera;
float savedT = CameraDiagnostics.TranslationStiffness;
float savedR = CameraDiagnostics.RotationStiffness;
try
{
CameraDiagnostics.AlignToSlope = true; // production default
CameraDiagnostics.CollideCamera = true;
CameraDiagnostics.TranslationStiffness = 0.45f;
CameraDiagnostics.RotationStiffness = 0.45f;
var cam = new RetailChaseCamera { CollisionProbe = new PassthroughProbe() };
// Live-like parked pose: static player, static yaw, zero velocity,
// grounded on a flat contact plane, ~1500 fps.
var playerPos = new Vector3(49.5f, -39.9f, -5.9f);
float yaw = 1.83f;
float dt = 1f / 1500f;
void Step() => cam.Update(
playerPosition: playerPos,
playerYaw: yaw,
playerVelocity: Vector3.Zero,
isOnGround: true,
contactPlaneNormal: Vector3.UnitZ,
dt: dt,
cellId: 0x8A020142u,
selfEntityId: 0x5);
// Converge: at α≈0.003/frame the boom needs a few thousand frames
// to settle from the init pose into the dead-band.
for (int i = 0; i < 20000; i++) Step();
Vector3 a = cam.Position;
var fwdA = cam.View; // full view matrix — includes the forward half
// 2000 further frames with bit-identical inputs: every one must be
// the exact fixed point (retail parks verbatim).
float maxDelta = 0f;
Vector3 prev = a;
bool viewChanged = false;
for (int i = 0; i < 2000; i++)
{
Step();
maxDelta = MathF.Max(maxDelta, Vector3.Distance(cam.Position, prev));
prev = cam.Position;
if (cam.View != fwdA) viewChanged = true;
}
_out.WriteLine(FormattableString.Invariant(
$"post-convergence maxConsecDelta={maxDelta * 1e6f:F2}um viewChanged={viewChanged} pos=({cam.Position.X:F7},{cam.Position.Y:F7},{cam.Position.Z:F7})"));
Assert.True(maxDelta == 0f,
$"parked camera must be a bit-exact fixed point, wandered up to {maxDelta * 1e6f:F1}um/frame");
Assert.False(viewChanged, "view matrix must be frozen at park");
}
finally
{
CameraDiagnostics.AlignToSlope = savedAlign;
CameraDiagnostics.CollideCamera = savedColl;
CameraDiagnostics.TranslationStiffness = savedT;
CameraDiagnostics.RotationStiffness = savedR;
}
}
}