perf(render): gate EnvCellRenderer visibility-snapshot rebuilds on real input changes (2026-07-24 audit review)
PrepareRenderBatches rebuilt the full EnvCell visibility snapshot every frame any indoor/building root was resolved: a Parallel.ForEach dispatch over every GpuReady EnvCell landblock (with per-landblock locks), a fresh outer dictionary plus one fresh inner dictionary per visible cell, a new snapshot object, and a complete transparency rescan - all while standing perfectly still. The NeedsPrepare flag existed since Phase A8 as the intended rebuild gate but was never read by production code. This wires the gate on the snapshot's actual inputs: - landblock commits/removals (the existing NeedsPrepare flag), - the visible-cell filter (content-compared; the caller reuses one scratch HashSet across frames), - the trim window (center/radius), - mesh render-data availability - new ObjectMeshManager.RenderDataAvailabilityVersion, bumped at publish, pending-release hide, release completion, and teardown, because the snapshot bakes per-cell transparency from TryGetRenderData and a late-arriving transparent shell must reclassify its cell, - the camera: eye position under a 1 mm ABSOLUTE epsilon (swallows the documented ~36 um rest jitter, dirties on any real movement; the VP translation row scales with AC's ~5e4 world coordinates where a relative tolerance would mask sub-meter motion) plus rows 1-3 of view*projection (position-independent rotation x projection) under relative 1e-5. Skipping is pool-safe: RenderCore re-anchors _poolIndex to the active snapshot's PostPreparePoolIndex on every call, so consecutive Renders without an intervening Prepare reuse scratch lists past the snapshot's owned region exactly as within-frame passes already do. The empty-filter branch is now also idempotent instead of allocating a fresh empty snapshot per frame. Render still receives the current frame's view-projection every frame (the U.4 stale-matrix rule) - only the snapshot rebuild is gated. Pixels must be identical; needs the standard user visual pass (dungeon + town-near-buildings) before the change is considered accepted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
38e14c6a71
commit
c2cb83d11f
3 changed files with 208 additions and 0 deletions
|
|
@ -262,4 +262,83 @@ public class EnvCellRendererTests
|
|||
Assert.Empty(a);
|
||||
Assert.Empty(b);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Prepare gate (2026-07-24) — pure camera-tolerance half.
|
||||
// The tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer
|
||||
// R-A2 note) but never survive real camera motion.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
private static Matrix4x4 ViewProjectionFor(Vector3 eye, Vector3 forward)
|
||||
{
|
||||
var view = Matrix4x4.CreateLookAt(eye, eye + forward, Vector3.UnitZ);
|
||||
var proj = Matrix4x4.CreatePerspectiveFieldOfView(
|
||||
fieldOfView: 1.2f, aspectRatio: 16f / 9f, nearPlaneDistance: 0.1f, farPlaneDistance: 2000f);
|
||||
return view * proj;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraApproximatelyEqual_IdenticalCamera_True()
|
||||
{
|
||||
var eye = new Vector3(120f, 80f, 10f);
|
||||
var vp = ViewProjectionFor(eye, Vector3.UnitX);
|
||||
Assert.True(EnvCellRenderer.CameraApproximatelyEqual(vp, eye, vp, eye));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraApproximatelyEqual_RestJitter_True()
|
||||
{
|
||||
// The ~36 µm eye rest jitter must NOT dirty the gate.
|
||||
var eye = new Vector3(120.34f, 87.91f, 10.2f);
|
||||
var jittered = eye + new Vector3(36e-6f, -36e-6f, 36e-6f);
|
||||
var a = ViewProjectionFor(eye, Vector3.UnitX);
|
||||
var b = ViewProjectionFor(jittered, Vector3.UnitX);
|
||||
Assert.True(EnvCellRenderer.CameraApproximatelyEqual(a, eye, b, jittered));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraApproximatelyEqual_SmallRealRotation_False()
|
||||
{
|
||||
// 0.05° of yaw — far below one frame of real mouse motion — must dirty.
|
||||
var eye = new Vector3(120.34f, 87.91f, 10.2f);
|
||||
float yaw = 0.05f * MathF.PI / 180f;
|
||||
var a = ViewProjectionFor(eye, Vector3.UnitX);
|
||||
var b = ViewProjectionFor(eye, new Vector3(MathF.Cos(yaw), MathF.Sin(yaw), 0f));
|
||||
Assert.False(EnvCellRenderer.CameraApproximatelyEqual(a, eye, b, eye));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraApproximatelyEqual_SmallRealTranslation_False()
|
||||
{
|
||||
// 5 cm of movement must dirty the gate.
|
||||
var eye = new Vector3(120.34f, 87.91f, 10.2f);
|
||||
var moved = eye + new Vector3(0.05f, 0f, 0f);
|
||||
var a = ViewProjectionFor(eye, Vector3.UnitX);
|
||||
var b = ViewProjectionFor(moved, Vector3.UnitX);
|
||||
Assert.False(EnvCellRenderer.CameraApproximatelyEqual(a, eye, b, moved));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraApproximatelyEqual_GlobalScaleCoordinates_TranslationStillDirties()
|
||||
{
|
||||
// AC world coordinates reach ~5e4. A relative tolerance applied to the
|
||||
// matrix translation row would mask sub-meter motion at that scale —
|
||||
// the eye epsilon is absolute precisely so this case stays sharp.
|
||||
var eye = new Vector3(40120.34f, 45087.91f, 110.2f);
|
||||
var moved = eye + new Vector3(0.07f, 0f, 0f); // one walking frame
|
||||
var a = ViewProjectionFor(eye, Vector3.UnitX);
|
||||
var b = ViewProjectionFor(moved, Vector3.UnitX);
|
||||
Assert.False(EnvCellRenderer.CameraApproximatelyEqual(a, eye, b, moved));
|
||||
|
||||
var jittered = eye + new Vector3(36e-6f, 0f, 0f);
|
||||
var c = ViewProjectionFor(jittered, Vector3.UnitX);
|
||||
Assert.True(EnvCellRenderer.CameraApproximatelyEqual(a, eye, c, jittered));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewRenderer_SnapshotGenerationStartsAtZero()
|
||||
{
|
||||
var r = new EnvCellRenderer(gl: null!, meshManager: null!, frustum: new WbFrustum());
|
||||
Assert.Equal(0, r.SnapshotGeneration);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue