acdream/src/AcDream.Plugin.Abstractions/Rendering/RenderPackDeclarations.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

489 lines
16 KiB
C#

namespace AcDream.Plugin.Abstractions.Rendering;
/// <summary>Prerequisite tier reached by a pack.</summary>
public enum RenderPackTier
{
Tier1 = 1,
Tier2 = 2,
Tier2Plus = 3,
}
/// <summary>
/// Renderer-owned facilities a pack may require or use opportunistically.
/// These are semantic capabilities, not Vulkan extension or feature names.
/// </summary>
public enum RenderCapability
{
MainWorldColorIntermediate,
FullscreenPasses,
SceneDepthSampling,
SceneNormalSampling,
AuthoredSunDirection,
AuthoredSunScreenPosition,
AuthoredWeather,
DirectionalShadowMaps,
OutdoorDirectionalShadowCasterReplay,
AnimatedCasterTransforms,
AlphaCutoutShadowCasters,
GpuTimestampQueries,
/// <summary>One layered directional-depth pass may address multiple cascade views.</summary>
MultiviewDirectionalShadowCascades,
/// <summary>One renderer-selected authored sun-or-moon shadow direction.</summary>
AuthoredCelestialDirectionalLight,
}
/// <summary>Fixed renderer-owned positions at which a declared pass may run.</summary>
public enum RenderPassHook
{
ShadowDepthBeforeWorld,
AtmosphereBeforeToneMap,
ToneMap,
AfterToneMapBeforePrivateViewports,
}
/// <summary>Immutable frame facts the renderer may bind for a pack.</summary>
public enum RenderSemanticInput
{
WorldColor,
SceneDepth,
SceneNormals,
SunDirection,
SunScreenPosition,
ActiveDayGroup,
Weather,
CameraMatrices,
ShadowCasterTransforms,
DirectionalShadowMaps,
FrameTime,
/// <summary>Selected surface-to-sun-or-moon direction for shadow work.</summary>
SelectedCelestialDirectionalLight,
}
/// <summary>Renderer-owned replay operations available to a declaration.</summary>
public enum RenderSceneReplaySemantic
{
OutdoorDirectionalShadowCasters,
}
/// <summary>Existing retained-scene classes eligible for a scene replay.</summary>
[Flags]
public enum RenderCasterClass
{
None = 0,
Terrain = 1 << 0,
OpaqueWorld = 1 << 1,
AlphaCutoutWorld = 1 << 2,
AnimatedOpaque = 1 << 3,
AnimatedAlphaCutout = 1 << 4,
}
/// <summary>Base renderer pipeline a pack may specialize.</summary>
public enum RenderPipelineBaseSemantic
{
Terrain,
WorldMesh,
EnvCell,
}
/// <summary>Material classifications accepted by a pipeline variant.</summary>
[Flags]
public enum RenderMaterialClass
{
None = 0,
Opaque = 1 << 0,
AlphaCutout = 1 << 1,
AnimatedOpaque = 1 << 2,
AnimatedAlphaCutout = 1 << 3,
}
/// <summary>Kind of renderer-owned intermediate resource.</summary>
public enum RenderResourceKind
{
Image2D,
Image2DArray,
Buffer,
}
/// <summary>Portable format families resolved by the renderer.</summary>
public enum RenderFormatClass
{
LdrColor,
HdrColor,
SingleChannel,
DirectionalDepth,
StructuredData,
}
/// <summary>How declared image dimensions are interpreted.</summary>
public enum RenderExtentMode
{
AbsolutePixels,
RelativeToMainWorld,
RelativeToOutput,
}
/// <summary>Permitted uses of a declared resource.</summary>
[Flags]
public enum RenderResourceUsage
{
None = 0,
Sampled = 1 << 0,
ColorAttachment = 1 << 1,
DepthAttachment = 1 << 2,
Storage = 1 << 3,
TransferSource = 1 << 4,
TransferDestination = 1 << 5,
}
/// <summary>Lifetime class used by the renderer's frame-flight allocator.</summary>
public enum RenderResourceLifetime
{
TransientPass,
FrameFlight,
ActivePack,
}
/// <summary>
/// Renderer-owned meaning of a declared resource. <see cref="Custom"/> is
/// available to ordinary declarative fullscreen graphs; the remaining values
/// let a pack request host executors without relying on magic resource IDs.
/// </summary>
public enum RenderResourceSemantic
{
Custom,
MainWorldHdr,
BloomPing,
BloomPong,
SunOcclusionMask,
SunRays,
DirectionalShadowDepth,
VolumetricShafts,
}
/// <summary>Shape of one image declaration.</summary>
/// <param name="Mode">Absolute pixels or a scale relative to a renderer surface.</param>
/// <param name="Width">Pixel width for absolute mode; horizontal scale otherwise.</param>
/// <param name="Height">Pixel height for absolute mode; vertical scale otherwise.</param>
/// <param name="Layers">Array layers; one for an ordinary 2-D image.</param>
public sealed record RenderExtentDeclaration(
RenderExtentMode Mode,
double Width,
double Height,
int Layers = 1);
/// <summary>One renderer-owned intermediate image or buffer.</summary>
public sealed record RenderResourceDeclaration(
string Id,
RenderResourceKind Kind,
RenderFormatClass Format,
RenderExtentDeclaration? Extent,
long SizeBytes,
RenderResourceUsage Usage,
RenderResourceLifetime Lifetime,
long EstimatedResidentBytes)
{
public RenderResourceSemantic Semantic { get; init; } = RenderResourceSemantic.Custom;
}
/// <summary>
/// Renderer-owned execution meaning of a pass. IDs remain pack-owned stable
/// identifiers; semantic execution never depends on a particular ID string.
/// </summary>
public enum RenderPassSemantic
{
CustomFullscreen,
DirectionalShadowDepth,
BloomDownsample,
BloomBlurHorizontal,
BloomBlurVertical,
SunOcclusion,
SunRays,
VolumetricShafts,
FilmicComposite,
}
/// <summary>One declarative full-screen, atmosphere, or tone-map pass.</summary>
public sealed record RenderPassDeclaration(
string Id,
RenderPassHook Hook,
string VertexShaderAsset,
string FragmentShaderAsset,
IReadOnlyList<RenderSemanticInput> SemanticInputs,
IReadOnlyList<string> ResourceReads,
IReadOnlyList<string> ResourceWrites)
{
public RenderPassSemantic Semantic { get; init; } = RenderPassSemantic.CustomFullscreen;
}
/// <summary>A renderer-owned replay of retained scene geometry.</summary>
public sealed record SceneReplayDeclaration(
string Id,
RenderSceneReplaySemantic Semantic,
RenderCasterClass CasterClasses,
int ViewCount);
/// <summary>A shader specialization of an existing renderer pipeline.</summary>
public sealed record PipelineVariantDeclaration(
string Id,
RenderPipelineBaseSemantic BaseSemantic,
string VertexShaderAsset,
string FragmentShaderAsset,
RenderMaterialClass CompatibleMaterials,
IReadOnlyList<RenderSemanticInput> SemanticInputs)
{
public RenderPipelineVariantSemantic Semantic { get; init; } =
RenderPipelineVariantSemantic.Custom;
}
/// <summary>Renderer-owned role of a fixed retained-scene pipeline variant.</summary>
public enum RenderPipelineVariantSemantic
{
Custom,
TerrainDirectionalShadowCaster,
WorldOpaqueDirectionalShadowCaster,
WorldAlphaCutoutDirectionalShadowCaster,
TerrainDirectionalShadowReceiver,
WorldDirectionalShadowReceiver,
TerrainMultiviewDirectionalShadowCaster,
WorldOpaqueMultiviewDirectionalShadowCaster,
WorldAlphaCutoutMultiviewDirectionalShadowCaster,
}
/// <summary>Per-preset replacement for one resource's size.</summary>
public sealed record RenderQualityResourceOverride(
string ResourceId,
RenderExtentDeclaration? Extent,
long SizeBytes,
long EstimatedResidentBytes);
/// <summary>Per-preset value for a declared user setting.</summary>
public sealed record RenderQualitySettingOverride(
string SettingId,
string Value);
/// <summary>One user-selectable, independently capability-gated preset.</summary>
public sealed record RenderQualityPreset(
string Id,
string DisplayName,
IReadOnlyList<RenderCapability> RequiredCapabilities,
IReadOnlyList<RenderQualityResourceOverride> ResourceOverrides,
IReadOnlyList<RenderQualitySettingOverride> SettingOverrides,
long MaxResidentGpuBytes,
double MaxIncrementalGpuMillisecondsP50,
double MaxIncrementalGpuMillisecondsP99,
double MaxIncrementalCpuMillisecondsP50,
double MaxIncrementalCpuMillisecondsP99,
bool AutoEligible = true)
{
public RenderQualitySemantic Semantic { get; init; } = RenderQualitySemantic.Custom;
/// <summary>
/// Optional renderer-owned execution optimizations whose shader ABI the
/// pack explicitly implements. The host never infers these from a pack ID.
/// </summary>
public RenderQualityExecutionHints ExecutionHints { get; init; } =
RenderQualityExecutionHints.None;
}
/// <summary>
/// Opt-in execution forms for renderer-owned atmospheric work. These hints
/// may fuse passes or compatible submissions; they do not remove declared
/// effects or caster classes from the final image.
/// </summary>
[Flags]
public enum RenderQualityExecutionHints
{
None = 0,
/// <summary>
/// The sun-rays shader accepts scene depth directly and filmic composite
/// evaluates the declared bloom extraction/filter while composing the
/// image. See the standard PackPass ABI flags.
/// </summary>
FusedAtmosphericPostProcess = 1 << 0,
/// <summary>
/// The three multiview caster variants select the exact cascade matrix with
/// the renderer-owned view index and render every declared Low cascade in
/// one layered depth pass.
/// </summary>
MultiviewDirectionalShadowCascades = 1 << 1,
}
/// <summary>
/// Optional host quality role. Pack-owned IDs remain persisted; this semantic
/// is used only when a pack opts into the host's automatic-quality controller.
/// </summary>
public enum RenderQualitySemantic
{
Custom,
Low,
Medium,
High,
Automatic,
}
/// <summary>Storage and presentation kind for a pack-defined setting.</summary>
public enum RenderSettingKind
{
Boolean,
Integer,
Float,
Choice,
}
/// <summary>A bounded, user-visible pack setting.</summary>
public sealed record RenderSettingDeclaration(
string Id,
string DisplayName,
RenderSettingKind Kind,
string DefaultValue,
double? Minimum,
double? Maximum,
double? Step,
IReadOnlyList<string> Choices)
{
public RenderSettingSemantic Semantic { get; init; } = RenderSettingSemantic.Custom;
}
/// <summary>
/// Optional host meaning for settings consumed by a renderer-owned atmospheric
/// executor. Ordinary pack settings use <see cref="Custom"/>.
/// </summary>
public enum RenderSettingSemantic
{
Custom,
BloomStrength,
FilmicStrength,
Exposure,
GradeSaturation,
GradeContrast,
VignetteStrength,
SunRayStrength,
DirectionalShadowStrength,
DirectionalShadowReachMetres,
DirectionalShadowPcfTaps,
VolumetricStrength,
VolumetricRayMarchSteps,
AutomaticQuality,
// Campaign VM VM6: foliage wind.
WindEnabled,
WindStrength,
WindDirectionDegrees,
WindLeanMetres,
WindBranchMetres,
WindFlutterMetres,
WindCanopyHeightMetres,
}
/// <summary>One point on a declared sun-elevation response curve.</summary>
public sealed record SunElevationResponsePoint(
double ElevationDegrees,
double Multiplier);
/// <summary>Explicit mapping from an authored AC day group to an effect multiplier.</summary>
public sealed record ActiveDayGroupMultiplier(
int ActiveDayGroup,
double Multiplier);
/// <summary>
/// Explicit mapping from a DAT-classified weather kind to the foliage-wind
/// mean and gust strength for that weather (both in the declared [0,1] range
/// before the pack's global wind-strength setting scales them). Campaign VM
/// VM6, corrected in the fix round: the day-group's raw <c>activeDayGroup</c>
/// INDEX carries no weather meaning by itself — <c>WeatherState.cs</c>'s
/// existing name classification is what turns a day group's DAT name into
/// one of the five real weather kinds, and that is what the host already
/// threads through <c>AtmosphericFrameInputs.Weather</c> / <c>uAtmosphereWeather.x</c>.
/// This is the same fact, keyed correctly instead of by index.
/// </summary>
/// <param name="WeatherKind">
/// The exact member name of <c>AcDream.Core.World.WeatherKind</c> — one of
/// <c>"Clear"</c>, <c>"Overcast"</c>, <c>"Rain"</c>, <c>"Snow"</c>,
/// <c>"Storm"</c>. A plain string because this project (Plugin.Abstractions)
/// is BCL-only and cannot reference the Core enum; the host converts with
/// <c>WeatherKind.ToString()</c> (ordinal) when resolving.
/// </param>
public sealed record FoliageWindWeatherPoint(
string WeatherKind,
double Mean,
double Gust);
/// <summary>Visible authored-atmosphere interpretation owned by the pack.</summary>
public sealed record AtmospherePolicyDeclaration(
IReadOnlyList<SunElevationResponsePoint> SunElevationResponse,
IReadOnlyList<ActiveDayGroupMultiplier> ActiveDayGroupMultipliers)
{
/// <summary>
/// Optional selected-light elevation curve for directional shadows. The host
/// linearly interpolates adjacent points in sine-of-elevation space and
/// clamps beyond the endpoints.
/// The resolved multiplier must be zero at and below the authored
/// 0-degree horizon: every non-positive control point must be zero and,
/// when no exact 0-degree point is declared, the first positive control
/// point must also be zero.
/// A pack using the directional-shadow semantic must declare this curve.
/// </summary>
public IReadOnlyList<SunElevationResponsePoint> DirectionalShadowLightElevationResponse
{ get; init; } = [];
/// <summary>
/// Optional moving-sun strength curve for volumetric shafts. The host
/// smoothstep-interpolates adjacent points in elevation-degree space and
/// clamps beyond the endpoints.
/// A pack using the volumetric-shaft semantic must declare this curve.
/// </summary>
public IReadOnlyList<SunElevationResponsePoint> VolumetricShaftSunElevationResponse
{ get; init; } = [];
/// <summary>
/// Campaign VM VM6: exact per-weather-kind foliage-wind mean/gust
/// targets, looked up by the DAT-classified
/// <c>AcDream.Core.World.WeatherKind</c> (an exact match, not an
/// interpolation — the five kinds are not ordered by "how windy"). The
/// host smooths the resolved (mean, gust) target toward whatever this
/// table returns over time using the existing weather delta-seconds
/// clock, so a weather change never snaps. A kind absent from this table
/// falls back to the declared Clear row, then to (0, 0) if Clear itself
/// is undeclared. Each declared <see cref="FoliageWindWeatherPoint.WeatherKind"/>
/// must be one of the five real kind names and must appear at most once.
/// </summary>
public IReadOnlyList<FoliageWindWeatherPoint> FoliageWindByWeather
{ get; init; } = [];
/// <summary>
/// Campaign VM VM6: object/GfxObj ids excluded from foliage-wind sway
/// even though their entity id falls in the procedural-scenery
/// namespace and their subset classification would otherwise qualify —
/// the rare scenery object that is cutout-material but not actually
/// foliage (e.g. a cutout rock or fence prop drawn from the scenery
/// table). There is no include list: outside this exclusion set, the
/// classification rule in <c>WbDrawDispatcher</c> is the rule.
/// </summary>
public IReadOnlyList<uint> FoliageExclusions { get; init; } = [];
}
/// <summary>
/// Complete immutable declaration for one render pack. Packs describe what
/// they need; the renderer validates and owns every concrete resource, pass,
/// pipeline, barrier, and scene replay.
/// </summary>
public sealed record RenderPackDescriptor(
string Id,
string DisplayName,
Version PackVersion,
int PackApiVersion,
RenderPackTier HighestTier,
IReadOnlyList<RenderCapability> RequiredCapabilities,
IReadOnlyList<RenderCapability> OptionalCapabilities,
IReadOnlyList<RenderResourceDeclaration> Resources,
IReadOnlyList<RenderPassDeclaration> Passes,
IReadOnlyList<SceneReplayDeclaration> SceneReplays,
IReadOnlyList<PipelineVariantDeclaration> PipelineVariants,
IReadOnlyList<RenderQualityPreset> QualityPresets,
IReadOnlyList<RenderSettingDeclaration> Settings,
AtmospherePolicyDeclaration? AtmospherePolicy)
{
/// <summary>Short user-facing description shown beside compatibility and cost.</summary>
public string FeatureSummary { get; init; } = string.Empty;
}