acdream/tests/AcDream.App.Tests/Rendering/Packs/RenderPackAtmospherePolicyEvaluationTests.cs
Erik 6cc5e183b9 fix(render): foliage wind keys on the DAT-classified WeatherKind, not the day-group index (Campaign VM VM6)
FoliageWindByDayGroup / FoliageWindDayGroupPoint(int ActiveDayGroup, ...)
becomes FoliageWindByWeather / FoliageWindWeatherPoint(string WeatherKind,
...) in AtmospherePolicyDeclaration (Plugin.Abstractions is BCL-only, so
the key is the exact member name of AcDream.Core.World.WeatherKind rather
than the enum itself). The raw activeDayGroup index carries no weather
meaning by itself; WeatherState.cs already classifies each day group's
authored DAT name into one of five real weather kinds, and that fact was
already threaded through AtmosphericFrameInputs.Weather / uAtmosphereWeather.x
— this reuses it instead of guessing an index-to-category mapping.

Built-in table (BuiltInAtmosphericRenderPack.AtmospherePolicy()): Clear
0.25/0.15, Overcast 0.60/0.35, Rain 0.85/0.60, Snow 0.35/0.20, Storm
1.00/0.75 — all five WeatherKind members declared, the invented "Cloudy"
row dropped. RenderPackAtmospherePolicyEvaluation.FoliageWind now takes a
WeatherKind and matches by weather.ToString() (ordinal) against each
declared point's name; a kind absent from the table falls back to the
declared Clear row, then to (0,0) if Clear itself is undeclared. The
delta-seconds EMA interpolation (EaseTowardTarget) is unchanged.
AtmosphericPostProcessGraph.ResolveFoliageWind and its two callers
(RenderPostProcess via inputs.Weather; RenderDirectionalShadows via
foundation.Atmosphere.Kind) now pass WeatherKind instead of the day-group
int.

RenderPackValidation.ValidateAtmosphere (runs for every pack declaring an
AtmospherePolicy, not gated to Tier2/shadow packs) now rejects an unknown
or non-exact-case weather-kind name and a repeated kind, mirroring the
existing ActiveDayGroupMultiplier duplicate-key check.

Tests: RenderPackAtmospherePolicyEvaluationTests rewritten for the
kind-keyed API (all five kinds resolve to their declared row, an unlisted
kind falls back to Clear, ordinal exact-case matching, null-table
handling); RenderPackSpirvValidatorTests gains four descriptor-validation
cases (unknown name, wrong case, duplicate kind, the five-kind table
accepted); AtmosphericPostProcessGraphTests' three foliage-wind cases now
select WeatherKind.Storm via `with` instead of an assumed day-group index.

Spot-check (per the coordinator's ask, not changed here): yes —
ActiveDayGroupMultiplier / EvaluateDayGroupPolicy (pre-existing, Campaign
AR/VM3-era — BuiltInAtmosphericRenderPack.AtmospherePolicy()'s three rows
`new ActiveDayGroupMultiplier(0, 1.0), (1, 0.35), (2, 0.20)`) key the
sun-ray/shadow/volumetric day-group strength multiplier by the same raw
activeDayGroup index with an undocumented assumed meaning (0=brightest ...
2=dimmest), the identical class of issue this commit fixes for foliage
wind. Left unchanged per instruction; flagging for the coordinator to file.

Full solution Debug and Release builds green. App hermetic filter
6024/6026 — the same 2 pre-existing failures as VM6a/VM6b. Both were
re-run in isolation per the verification ask: both still fail alone (not
a load-flake in this environment) — confirmed via git stash earlier this
session that both already fail on the unmodified pre-VM6 baseline, so
they are pre-existing and unrelated to this change. Core.Tests hermetic
4697/4697. RenderPackValidator.Tests 30/30. No shader/spv changes in this
commit (pure C#/docs fix).

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

191 lines
6.5 KiB
C#

using AcDream.App.Rendering.Packs;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Tests.Rendering.Packs;
/// <summary>
/// Campaign VM VM6: <see cref="RenderPackAtmospherePolicyEvaluation.FoliageWind"/>
/// (the exact per-weather-kind mean/gust lookup, keyed by the DAT-classified
/// <see cref="WeatherKind"/> — corrected in the fix round from the raw
/// <c>activeDayGroup</c> index, which carries no weather meaning by itself)
/// and <see cref="RenderPackAtmospherePolicyEvaluation.EaseTowardTarget"/>
/// (the smoothing step that keeps a weather change from snapping).
/// </summary>
public sealed class RenderPackAtmospherePolicyEvaluationTests
{
private static readonly FoliageWindWeatherPoint[] FiveKindTable =
[
new("Clear", 0.25, 0.15),
new("Overcast", 0.60, 0.35),
new("Rain", 0.85, 0.60),
new("Snow", 0.35, 0.20),
new("Storm", 1.00, 0.75),
];
[Theory]
[InlineData(WeatherKind.Clear, 0.25f, 0.15f)]
[InlineData(WeatherKind.Overcast, 0.60f, 0.35f)]
[InlineData(WeatherKind.Rain, 0.85f, 0.60f)]
[InlineData(WeatherKind.Snow, 0.35f, 0.20f)]
[InlineData(WeatherKind.Storm, 1.00f, 0.75f)]
public void FoliageWindResolvesEachOfTheFiveDeclaredWeatherKinds(
WeatherKind kind,
float expectedMean,
float expectedGust)
{
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
FiveKindTable,
kind);
Assert.Equal(expectedMean, mean);
Assert.Equal(expectedGust, gust);
}
[Fact]
public void FoliageWindFallsBackToTheClearRowForAnUnlistedKind()
{
// A table that only declares Clear and Storm — a kind absent from
// the table (Overcast here) falls back to the declared Clear row,
// not to (0, 0), since an unresolved weather kind is closer to "no
// weather data" than to "assume it's calm."
FoliageWindWeatherPoint[] table =
[
new("Clear", 0.25, 0.15),
new("Storm", 1.00, 0.75),
];
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
table,
WeatherKind.Overcast);
Assert.Equal(0.25f, mean);
Assert.Equal(0.15f, gust);
}
[Fact]
public void FoliageWindReturnsZeroWhenNeitherTheKindNorClearIsDeclared()
{
FoliageWindWeatherPoint[] table = [new("Storm", 1.00, 0.75)];
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
table,
WeatherKind.Overcast);
Assert.Equal(0f, mean);
Assert.Equal(0f, gust);
}
[Fact]
public void FoliageWindReturnsZeroForANullTable()
{
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
null,
WeatherKind.Clear);
Assert.Equal(0f, mean);
Assert.Equal(0f, gust);
}
[Fact]
public void FoliageWindMatchesByExactNameNotSubstringOrCase()
{
// "Rain" must not match "Rainy" or any case variant — the lookup is
// an ordinal exact match against the WeatherKind member name.
FoliageWindWeatherPoint[] table =
[
new("Rainy", 0.99, 0.99),
new("rain", 0.99, 0.99),
new("Clear", 0.10, 0.05),
];
(float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(
table,
WeatherKind.Rain);
// Neither "Rainy" nor "rain" matches WeatherKind.Rain.ToString()
// ("Rain", exact case) — falls back to the declared Clear row.
Assert.Equal(0.10f, mean);
Assert.Equal(0.05f, gust);
}
[Fact]
public void EaseTowardTargetDoesNotMoveWithZeroDelta()
{
float result = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
current: 0.25f,
target: 0.85f,
deltaSeconds: 0f,
transitionSeconds: 10f);
Assert.Equal(0.25f, result);
}
[Fact]
public void EaseTowardTargetConvergesWithoutOvershootOrDiscontinuity()
{
// Sample the same weather change (Clear 0.25 -> Rainy 0.85) many
// times over the transition window at a fixed per-tick delta and
// assert the sequence is monotonically non-decreasing and never
// jumps by more than one tick's proportional share — "no
// discontinuity across the delta window."
const float start = 0.25f;
const float target = 0.85f;
const float transitionSeconds = 10f;
const float tickSeconds = 0.1f;
float current = start;
float previous = current;
// A fixed-rate-per-tick ease is exponential decay toward the
// target, not a linear ramp: after N ticks the remaining gap is
// (1 - tickSeconds/transitionSeconds)^N of the original gap. 4000
// ticks (400 s of simulated time, 40x the transition window) leaves
// a remaining fraction of (0.99)^4000 ~= 4e-18 — comfortably
// converged without asserting a false linear-ramp expectation.
const int ticks = 4000;
for (int i = 0; i < ticks; i++)
{
current = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
current,
target,
tickSeconds,
transitionSeconds);
Assert.True(
current >= previous - 1e-6f,
$"tick {i}: value regressed from {previous} to {current}");
Assert.True(
current <= target + 1e-6f,
$"tick {i}: value {current} overshot target {target}");
previous = current;
}
Assert.True(MathF.Abs(current - target) < 0.01f, $"did not converge: {current}");
}
[Fact]
public void EaseTowardTargetSnapsInOneStepWhenTransitionSecondsIsZeroOrLess()
{
float result = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
current: 0f,
target: 1f,
deltaSeconds: 0.001f,
transitionSeconds: 0f);
Assert.Equal(1f, result);
}
[Fact]
public void EaseTowardTargetClampsAnOversizedDeltaInsteadOfOvershooting()
{
// A delta larger than the transition window (e.g. after a long
// pause) must not overshoot past the target.
float result = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget(
current: 0f,
target: 1f,
deltaSeconds: 1000f,
transitionSeconds: 10f);
Assert.Equal(1f, result);
}
}