acdream/tests/AcDream.App.Tests/Rendering/Wb/PackedDispatcherOracleTests.cs
Erik fccba8390d refactor(render): one group-creation seam, required foliage key field, first-frame wind snap (Campaign VM VM6 review 3)
Narrow re-review of a82959f1: APPROVE, with follow-ups. All items landed.

N2 (structural): (a) extracted the ONE shared InstanceGroup-from-key
construction seam, WbDrawDispatcher.CreateGroupFromKey(key, registration,
frame) — before this there were two near-identical `new InstanceGroup
{ ... }` initializers (GetOrCreateInstanceGroup and GetOrCreatePackedGroup)
that had already drifted once (the round-2 F1 bug). Both routes call it now;
CreateGroupFromKey's own `new()` is the only production InstanceGroup
construction site repo-wide, same precedent as AppendPackedInstance. (b)
GroupKey.FoliageFlags lost its `= 0u` default and moved before CullMode in
the declaration (CullMode keeps its default, C# requires optional params to
trail required ones), so a `new GroupKey(...)` that omits it is a compile
error. Fixed every real construction site the reorder/requirement touched:
the 2 production sites, ToKey (a reconstruction from InstanceGroup the
review didn't count but the reorder broke), and 5 test sites (one more than
the review's "4" — InstanceGroupClearTests had a second, implicit
target-typed `MakeKey` factory the original count missed). Verified by a
full solution build.

N1: added CreateGroupFromKey_CopiesFoliageFlagsFromTheKey
(InstanceGroupClearTests) — a key carrying FoliageFlags 0x2 in, the created
group's FoliageFlags 0x2 out. That test plus N2b's required field are what
actually guard the round-2 F1 blocker; reworded PackedDispatcherOracleTests'
existing test comment to say what IT proves (the classification-to-
BuildIndirectArrays-to-BatchData.flags path), not that it guards the
classifier.

N3: corrected the plan's round-2 paragraph — folding FoliageFlags into the
G2/G3 digest is correct and symmetric, but CompareClassifiedOutput only
runs from RenderScenePViewFrameProductController.BuildAndCompare, which has
no production caller anywhere in src/AcDream.App/, and both of
RenderScenePViewFrameProductTests's own callers construct the controller
without the optional dispatcher argument — so the fold catches nothing
until that oracle is wired to an actual caller.

N4: the plan's F6 note now names both classification caches — the classic
route's EntityClassificationCache.EntityCacheEntry (self-heals per entity
on its own next eviction) and the packed route's
PackedProjectionClassificationEntry/PackedClassifiedBatch.Key
(PackedProjectionClassificationCache.BeginFrame clears its entire cache in
one shot on a RenderSceneGeneration change) — and notes neither mechanism
is keyed to a pack switch specifically.

N5: deleted the now-unused single-generic ComputeEntityHasCutoutSubset<T>
overload; its 4 test call sites now use the two-generic, zero-alloc
overload with an unused int context and a static (_, value) => value
lambda, so there is exactly one ComputeEntityHasCutoutSubset to keep
correct.

A6 (reviewer-filed): ResolveFoliageWind's _windMean/_windGust started at 0
and always eased toward the weather target by clock delta, with no
distinction for a graph's first-ever advance. A pinned clock
(ACDREAM_SKY_PHASE_SECONDS, the offline pixel gate's determinism pin) never
advances between calls, so the wind reached only whatever fraction the
first (1-second-clamped) step produced and sat there forever; live, the
first 10 s after a graph is constructed (pack selection / login) spun up
from dead calm even though the weather already IS what it is. Fixed at the
root: the first advance (_windFrameSerial == -1, the constructor sentinel)
now snaps _windMean/_windGust straight to the target; every later advance
eases over WeatherSystem.TransitionSeconds exactly as before. Added a
SetWindClockSecondsOverrideForTesting seam (_windClockSecondsOverride is no
longer readonly) so a hermetic test can advance the pinned clock by an
exact amount between two resolves without a real-time Thread.Sleep; two new
tests prove the first-advance snap is exact and a second advance still
eases at the normal rate. The three existing indoor/wind-disabled/amplitude
gate tests pass unchanged.

Verify: Release build 0 warnings/0 errors. App hermetic-lane filter
6,046/0 failed. Core.Tests 4,695/0 failed. Full hermetic-filtered solution:
15,274/0 failed across 15 projects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 03:03:56 +02:00

115 lines
4.4 KiB
C#

using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Gpu;
using AcDream.Core.Meshing;
using System.Numerics;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class PackedDispatcherOracleTests
{
/// <summary>
/// Campaign VM VM6 review fix round 3 (N1): this test does NOT guard
/// the packed classifier itself — CreateGroupFromKey's own test
/// (InstanceGroupClearTests.CreateGroupFromKey_CopiesFoliageFlagsFromTheKey)
/// plus GroupKey.FoliageFlags being a required constructor argument are
/// what actually make F1 (the round-2 blocker: the packed classifier's
/// InstanceGroup silently carried FoliageFlags == 0) impossible to
/// reintroduce. What THIS test proves is the SHARED production step
/// downstream of classification: FoliageWindClassification.Classify,
/// called with the same argument shape ClassifyPackedBatches uses
/// (entity.LocalEntityId, exclusion membership, batch.Translucency,
/// entity-scoped HasCutoutSubset), resolves a real procedural-scenery
/// entity id (0x8…) to CutoutFoliageFlag (0x2); and a group carrying
/// that flags value reaches the literal BatchData.flags word through
/// BuildIndirectArrays — the same already-tested production step both
/// the classic and packed group lists feed into
/// (WbDrawDispatcherIndirectBuilderTests pins bit 0; this pins bits 1/2
/// landing alongside it for a real scenery id).
/// </summary>
[Fact]
public void SceneryCutoutEntityClassificationReachesTheBatchDataFlagsWordAsBit0x2()
{
const uint proceduralSceneryTreeId = 0x80010203u; // top nibble 0x8
uint foliageFlags = FoliageWindClassification.Classify(
proceduralSceneryTreeId,
isExcluded: false,
TranslucencyKind.ClipMap,
meshHasCutoutSubset: true);
Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, foliageFlags);
Assert.Equal(0x2u, foliageFlags);
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
{
new(
IndexCount: 12,
FirstIndex: 0,
BaseVertex: 0,
InstanceCount: 1,
FirstInstance: 0,
TextureIndex: 0x5,
TextureLayer: 0,
Translucency: TranslucencyKind.ClipMap,
FoliageFlags: foliageFlags),
};
var indirect = new DrawElementsIndirectCommand[4];
var batches = new WbDrawDispatcher.BatchDataPublic[4];
WbDrawDispatcher.BuildIndirectArrays(groups, indirect, batches);
// Bit 0 (the #226 built-mesh marker) | bit 1 (cutout foliage) = 0x3;
// isolating bit 1 with a mask proves the foliage classification
// specifically reached the word, not merely that SOME nonzero value
// did.
Assert.Equal(0x2u, batches[0].Flags & 0x2u);
}
[Theory]
[InlineData(false, 0u)]
[InlineData(true, 1u)]
public void PackedInstanceWriter_AppendsDetailCategoryInParallel(
bool buildingDetail,
uint expectedCategory)
{
var group = new WbDrawDispatcher.InstanceGroup();
WbDrawDispatcher.AppendPackedInstance(
group,
Matrix4x4.Identity,
Vector3.One,
submissionOrder: 7,
slot: 3u,
lights: WbDrawDispatcher.InstanceLightSet.Disabled,
indoor: true,
buildingDetail: buildingDetail,
opacity: 0.5f,
selectionLighting: new Vector2(0.25f, 0.75f));
Assert.Single(group.Matrices);
Assert.Single(group.LocalSortCenters);
Assert.Single(group.SubmissionOrders);
Assert.Single(group.Slots);
Assert.Single(group.LightSets);
Assert.Single(group.IndoorFlags);
Assert.Equal(expectedCategory, Assert.Single(group.DetailCategories));
Assert.Single(group.Opacities);
Assert.Single(group.SelectionLighting);
}
[Theory]
[InlineData(0u, false, false)]
[InlineData(0u, true, false)]
[InlineData(7u, false, false)]
[InlineData(7u, true, true)]
public void TransparentDeferral_MatchesProductionNoVaoEarlyReturn(
uint anyVao,
bool alphaQueueCollecting,
bool expected)
{
Assert.Equal(
expected,
WbDrawDispatcher.ShouldDeferPackedTransparent(
anyVao,
alphaQueueCollecting));
}
}