acdream/src/AcDream.App/Rendering/Wb/GroupKey.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

52 lines
2.7 KiB
C#

using AcDream.App.Rendering.Gpu;
using AcDream.Core.Meshing;
using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Bucket identity for <see cref="WbDrawDispatcher"/>'s per-frame group dictionary.
/// Two (entity, batch) pairs that share the same <see cref="GroupKey"/> render
/// in a single <c>glMultiDrawElementsIndirect</c> draw command. Promoted to
/// <c>internal</c> at file scope (was a private nested type) so
/// <see cref="EntityClassificationCache"/> can store it inside <see cref="CachedBatch"/>
/// without depending on dispatcher internals.
///
/// <para>Campaign V slice V4t replaced the raw 64-bit
/// <c>ARB_bindless_texture</c> handle with <see cref="GpuTextureSlot"/>. The
/// substitution is a bijection — the device interns one slot per resident
/// handle — so exactly the same (entity, batch) pairs bucket together as
/// before, which is the property that keeps submission order identical. This
/// key's VALUE never orders anything: groups are enumerated in the dictionary's
/// insertion order and sorted by cull mode then camera distance
/// (<c>CompareOpaqueSubmissionOrder</c> / <c>CompareTransparentSubmissionOrder</c>),
/// and the delayed-alpha path sorts by viewer distance then submission ordinal.
/// The key reaches only equality, hashing, and the scene-digest fingerprints.</para>
///
/// <para>Campaign VM VM6 review fix round: <see cref="FoliageFlags"/> joined
/// the key so a mesh subset reachable from BOTH a procedural-scenery entity
/// and a non-scenery entity (same index range/texture/translucency/cull
/// mode) buckets into two DIFFERENT groups instead of coalescing into one
/// group whose <c>InstanceGroup.FoliageFlags</c> was last-writer-wins between
/// the two classifications — which flickered the shared group's flags
/// between frames and let the caster (keyed correctly from the start) and
/// the receiver (previously keyed without this field) disagree about the
/// same subset.</para>
///
/// <para>Campaign VM VM6 review fix round 3 (N2b): <see cref="FoliageFlags"/>
/// is REQUIRED (no default) and declared before <see cref="CullMode"/> (which
/// keeps its default) so that a <c>new GroupKey(...)</c> omitting it is a
/// compile error, not a silent fall-back to 0. The round-2 packed-classifier
/// blocker (F1) was exactly a silently-defaulted FoliageFlags reaching
/// production; a caller can no longer forget this field the way that one
/// could.</para>
/// </summary>
internal readonly record struct GroupKey(
uint FirstIndex,
int BaseVertex,
int IndexCount,
GpuTextureSlot TextureSlot,
uint TextureLayer,
TranslucencyKind Translucency,
uint FoliageFlags,
CullMode CullMode = CullMode.CounterClockwise);