fix(rendering): carry retail SetSurface state to detail draws

Resolve exact SetSurface blend, alpha-test, and fog state once during extraction and preserve it through recipe-10 prepared payloads, Wb/EnvCell command data, Vulkan pipelines, push constants, and both ordinary/atmospheric one-pass shaders. Preserve AP-240 Wb pure-Clip immediate opaque/A2C while EnvCell uses retail premultiplied Clip; detail-off routing remains unchanged. Correct AP-232 and register the remaining detail-off state divergence.

Mutation first failures (all restored):
- raw Add->SRCALPHA/ONE: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState line 1278, expected wb-mesh-raw-additive-1x.
- inverse-add->alpha-add: same test line 1278, expected wb-mesh-inverse-additive-1x.
- remove Env inverse: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 132, expected envcell-inverse.
- raw IsAdditive precedence: same test line 132, Translucent|Clip|Additive expected envcell-alpha.
- Wb paletted ParamB=0: OrderPreservingSubmitterTests line 333, expected 0.392156869.
- Wb DDS ParamB=0.05: same test line 333, expected 0.784313738.
- disable Alpha+Clip test: WalkStaticStreamPopulatorTests line 1286, expected 0.784313738.
- always fog raw Add: same test line 1287, expected no-fog true.
- disable fog non-Add: same test line 1287, expected no-fog false.
- X=a*qA: RetailDetailTextureContractTests line 261, shared squared-alpha substring absent.
- X includes base alpha: same test line 262, forbidden baseTexel.a present.
- CLIP uses 0.05: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 367.
- second detail draw: EnvCell detail-on line 131, collection contained 2 draws.
- straight-alpha substitute: Wb immediate line 1278, expected wb-mesh-additive-1x.
- omit ordered detail arm: OrderPreservingSubmitterTests line 318, expected (77,3.5), got (0,0).
- omit atmospheric combine: RetailDetailTextureContractTests line 260, shared include absent.
- drop serialized opacity: ObjectMeshDataSerializerTests line 292, expected opacity bits, got 1.0.
- stale detail arm: atmospheric adjacency line 402, expected slot 0, got 77.
- per-frame surface map: EnvCell warmed allocation line 285, expected 0 B, got 204800 B.
This commit is contained in:
Erik 2026-09-05 00:56:52 +02:00
parent 75664805f8
commit 15ed57a1e7
44 changed files with 1154 additions and 187 deletions

View file

@ -259,8 +259,22 @@ heading/velocity/parent state, arms no teleport hook, and still acknowledges
immediately. The long AP-148 row below is retained only as historical filing immediately. The long AP-148 row below is retained only as historical filing
research and is no longer active. research and is no longer active.
**S5-c4 fix-round-1 AP-232 correction (2026-09-05):** AP-244 below raises
the active total to **161**; the historical heading's 160 is superseded.
AP-232's retirement remains valid for the old two-draw/detail-weight
deviation, with this corrected scope: recipe 10 carries extraction-resolved
SetSurface blend, alpha-test/reference, and fog state through both real command
owners. Detail-active Wb and EnvCell select raw Additive, Alpha+Additive,
inverse, inverse-additive, combined/pure ClipMap, late Translucent override,
final-X reference, and raw-Additive fog-disable state exactly. Wb pure Clip
retains AP-240's immediate opaque/A2C pipeline while gaining final-X ParamB;
only EnvCell pure Clip uses `ONE/INVSRCALPHA`. The retired row's recipe-9-only
and `None` residual wording is superseded by this correction and AP-244;
AP-238/AP-239/AP-240 remain separate queue/token/raw-mask/placement residuals.
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| AP-244 | **Filed 2026-09-05 at Campaign OVERHAUL S5-c4 fix round 1.** The new exact resolved SetSurface state is deliberately selected only when a Wb/EnvCell command has active building detail, preserving §23's bounded detail-off behavior. With detail off, Wb still collapses raw Additive and InvAlpha+Additive through `TranslucencyKind.Additive` to `SRCALPHA,ONE`; EnvCell additionally selects raw `IsAdditive` before the late `Translucent\|ClipMap` override and has no detail-off inverse pipeline. Both shader families retain the existing non-detail 0.05 discard rather than every combined ClipMap's palette/DDS reference, and fixed-function fog remains enabled for raw-Additive detail-off rows. AP-240 separately covers Wb pure-Clip placement/A2C. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`PipelineForBlend`, detail gates); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`ResolveBatchGroupIndex`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (detail-active exact selection versus retained range buckets); `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag` | Fix round 1 was expressly bounded to detail-active one-pass material truth. The immutable state is already present end-to-end, so a later dedicated detail-off correction can select it without DAT lookup or new ownership, but must re-gate FIFO/routing/order and visual behavior together. | Detail-off raw Additive/inverse-additive brightness, inverse EnvCell compositing, combined ClipMap cutout threshold, late Translucent override, and raw-Additive fog can differ from retail. | `D3DPolyRender::SetSurface @0x0059C4D0` lines 425083425303; Campaign OVERHAUL S5-c4 §23.2 |
| AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001``ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` | | AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001``ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` |
| AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` | | AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` |
| ~~AP-232~~ | **RETIRED 2026-09-04 at Campaign OVERHAUL S5-c4.** The acdream-only base-plus-detail replay is deleted. `TextureBatchData.SurfaceOpacity` carries exact `1-Surface.Translucency` from `MeshExtractor` through recipe-9 prepared serialization, `ObjectRenderBatch`, and the unchanged 16-byte GPU batch ABI. `mesh_modern` and `mesh_atmospheric` share `retail_detail_material.glsl` and emit one pre-fog fragment per detail-active subset: `w=a*qA`, `C=q*w+(B*D)*(1-w)`, `X=a*qA*qA`, where `a=authoredOpacity*liveFade`; base texture alpha is excluded. The final `X` drives pure CLIP at 100/255 or 200/255 with equality surviving. Classic grouped, walk-ordered (#471), immediate alpha, delayed alpha, and EnvCell paths keep their original draw position and pipeline family; the separate RetailDetail pipelines, replay helpers, shaders, SPIR-V, and manifest entry are gone. | `src/AcDream.Content/MeshExtractor.cs`; `src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs`; `src/AcDream.App/Rendering/Shaders/retail_detail_material.glsl`; `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs`; `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` | Exact retail single-pass stage arithmetic, authored-alpha provenance, framebuffer-family/CLIP CPU fixtures, shader ABI pins, deterministic recipe-9 round trips and rejection of recipe 8, one-draw command transcripts, shader/SPIR-V validation, and 0-B warmed-path proof. | None for the former weight/draw-count deviation. AP-238/AP-239/AP-240 remain separate queue/token/raw-mask placement residuals and are not changed by this retirement. | `D3DPolyRender::SetSurface @0x0059C4D0`; `RenderMeshSubset @0x0059CA10`; `DrawMesh @0x0059D4A0`; VM2 cdb note `docs/research/2026-08-22-vm2-retail-detail-path-cdb.md`; Campaign OVERHAUL S5-c4 §22 | | ~~AP-232~~ | **RETIRED 2026-09-04 at Campaign OVERHAUL S5-c4.** The acdream-only base-plus-detail replay is deleted. `TextureBatchData.SurfaceOpacity` carries exact `1-Surface.Translucency` from `MeshExtractor` through recipe-9 prepared serialization, `ObjectRenderBatch`, and the unchanged 16-byte GPU batch ABI. `mesh_modern` and `mesh_atmospheric` share `retail_detail_material.glsl` and emit one pre-fog fragment per detail-active subset: `w=a*qA`, `C=q*w+(B*D)*(1-w)`, `X=a*qA*qA`, where `a=authoredOpacity*liveFade`; base texture alpha is excluded. The final `X` drives pure CLIP at 100/255 or 200/255 with equality surviving. Classic grouped, walk-ordered (#471), immediate alpha, delayed alpha, and EnvCell paths keep their original draw position and pipeline family; the separate RetailDetail pipelines, replay helpers, shaders, SPIR-V, and manifest entry are gone. | `src/AcDream.Content/MeshExtractor.cs`; `src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs`; `src/AcDream.App/Rendering/Shaders/retail_detail_material.glsl`; `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs`; `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` | Exact retail single-pass stage arithmetic, authored-alpha provenance, framebuffer-family/CLIP CPU fixtures, shader ABI pins, deterministic recipe-9 round trips and rejection of recipe 8, one-draw command transcripts, shader/SPIR-V validation, and 0-B warmed-path proof. | None for the former weight/draw-count deviation. AP-238/AP-239/AP-240 remain separate queue/token/raw-mask placement residuals and are not changed by this retirement. | `D3DPolyRender::SetSurface @0x0059C4D0`; `RenderMeshSubset @0x0059CA10`; `DrawMesh @0x0059D4A0`; VM2 cdb note `docs/research/2026-08-22-vm2-retail-detail-path-cdb.md`; Campaign OVERHAUL S5-c4 §22 |

View file

@ -132,6 +132,12 @@ internal enum GpuBlendMode
/// <summary>Additive: <c>SrcAlpha, One</c>.</summary> /// <summary>Additive: <c>SrcAlpha, One</c>.</summary>
Additive, Additive,
/// <summary>Retail raw Additive: <c>One, One</c>.</summary>
RawAdditive,
/// <summary>Retail inverse-additive: <c>OneMinusSrcAlpha, One</c>.</summary>
InverseAdditive,
/// <summary> /// <summary>
/// Retail's inverse-alpha translucency: <c>OneMinusSrcAlpha, SrcAlpha</c>. /// Retail's inverse-alpha translucency: <c>OneMinusSrcAlpha, SrcAlpha</c>.
/// ///

View file

@ -176,6 +176,8 @@ internal static class VulkanViewportMapping
GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha), GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
GpuBlendMode.PremultipliedAlpha => (BlendFactor.One, BlendFactor.OneMinusSrcAlpha), GpuBlendMode.PremultipliedAlpha => (BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One), GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
GpuBlendMode.RawAdditive => (BlendFactor.One, BlendFactor.One),
GpuBlendMode.InverseAdditive => (BlendFactor.OneMinusSrcAlpha, BlendFactor.One),
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend. // Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha), GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero), GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero),

View file

@ -34,6 +34,8 @@ namespace AcDream.App.Rendering;
/// </summary> /// </summary>
internal static class RetailDetailTextureContract internal static class RetailDetailTextureContract
{ {
internal const int NoFogRenderPassFlag = 0x200;
internal static bool ShouldRender( internal static bool ShouldRender(
bool settingEnabled, bool settingEnabled,
TerrainAtlas.RetailDetailTextureBinding binding) => TerrainAtlas.RetailDetailTextureBinding binding) =>

View file

@ -128,9 +128,12 @@ void main() {
alpha = color.a * vOpacityMultiplier; alpha = color.a * vOpacityMultiplier;
} }
if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff) if (detailActive
? isRetailClipReference(uParamB) && alpha < uParamB
: color.a < alphaCutoff)
discard; discard;
if (!detailActive || (uRenderPass & 0x200) == 0)
rgb = applyFog(rgb, vWorldPos); rgb = applyFog(rgb, vWorldPos);
FragColor = vec4(rgb, alpha); FragColor = vec4(rgb, alpha);
} }

View file

@ -25,6 +25,7 @@ in flat uint vDetailCategory;
// discard alpha >= 0.95 (already drawn opaque) and // discard alpha >= 0.95 (already drawn opaque) and
// alpha < 0.05 (skip empty fragments — large // alpha < 0.05 (skip empty fragments — large
// transparent overdraw cost otherwise) // transparent overdraw cost otherwise)
// bit 0x200 = detail-active raw Additive; bypass fixed-function fog
uniform int uRenderPass; uniform int uRenderPass;
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
// S4-c2 F3: only retail's exact pure-ClipMap alpha-test references are // S4-c2 F3: only retail's exact pure-ClipMap alpha-test references are
@ -120,9 +121,12 @@ void main() {
// '<' spells D3DCMP_GREATEREQUAL: equality passes. Detail-active CLIP // '<' spells D3DCMP_GREATEREQUAL: equality passes. Detail-active CLIP
// tests retail's final stage-1 alpha; detail-off keeps base-alpha logic. // tests retail's final stage-1 alpha; detail-off keeps base-alpha logic.
if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff) if (detailActive
? isRetailClipReference(uParamB) && alpha < uParamB
: color.a < alphaCutoff)
discard; discard;
if (!detailActive || (uRenderPass & 0x200) == 0)
rgb = applyFog(rgb, vWorldPos); rgb = applyFog(rgb, vWorldPos);
FragColor = vec4(rgb, alpha); FragColor = vec4(rgb, alpha);
} }

View file

@ -220,7 +220,7 @@
}, },
{ {
"stage": "frag", "stage": "frag",
"sourceSha256": "5eb058e92ab27f1f6c245401e05f78a278fe00696b5c77f36ae33f8428f83bf3", "sourceSha256": "c95e372c557a8a4b252ecf824ad049fe9611b0a1e49b0e6d18938d373d9c6794",
"compiled": true "compiled": true
} }
] ]
@ -236,7 +236,7 @@
}, },
{ {
"stage": "frag", "stage": "frag",
"sourceSha256": "396c382af73505bc2cac7a2db5878a63166730e47096a94630c35ad8376fe565", "sourceSha256": "e145f32cbd50edcbc5e3196a49f0e2b05548bc0199ce13139e01c443794473b0",
"compiled": true "compiled": true
} }
] ]

View file

@ -2,6 +2,7 @@ using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu;
using AcDream.Core.Lighting; using AcDream.Core.Lighting;
using AcDream.Core.Meshing;
using DatReaderWriter.Enums; using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb; namespace AcDream.App.Rendering.Wb;
@ -28,6 +29,9 @@ public sealed unsafe partial class EnvCellRenderer
private IGpuPipeline? _alphaPipeline; private IGpuPipeline? _alphaPipeline;
private IGpuPipeline? _clipPipeline; private IGpuPipeline? _clipPipeline;
private IGpuPipeline? _additivePipeline; private IGpuPipeline? _additivePipeline;
private IGpuPipeline? _rawAdditivePipeline;
private IGpuPipeline? _inversePipeline;
private IGpuPipeline? _inverseAdditivePipeline;
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail; private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
private readonly Func<bool> _buildingDetailEnabled; private readonly Func<bool> _buildingDetailEnabled;
@ -48,7 +52,7 @@ public sealed unsafe partial class EnvCellRenderer
/// <summary> /// <summary>
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the /// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
/// four pipelines ARE this renderer's program, so there is no second step /// SetSurface pipeline family IS this renderer's program, so there is no second step
/// and no <c>Shader</c> to hand in. /// and no <c>Shader</c> to hand in.
/// </summary> /// </summary>
internal EnvCellRenderer( internal EnvCellRenderer(
@ -80,6 +84,12 @@ public sealed unsafe partial class EnvCellRenderer
scope.SampleCount); scope.SampleCount);
_additivePipeline = CreateShellPipeline( _additivePipeline = CreateShellPipeline(
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount); device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
_rawAdditivePipeline = CreateShellPipeline(
device, "envcell-raw-additive", GpuBlendMode.RawAdditive, depthWrite: false, scope.SampleCount);
_inversePipeline = CreateShellPipeline(
device, "envcell-inverse", GpuBlendMode.InverseAlpha, depthWrite: false, scope.SampleCount);
_inverseAdditivePipeline = CreateShellPipeline(
device, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, depthWrite: false, scope.SampleCount);
_initialized = true; _initialized = true;
} }
@ -272,7 +282,9 @@ public sealed unsafe partial class EnvCellRenderer
: isAdditive : isAdditive
? _additivePipeline! ? _additivePipeline!
: _alphaPipeline!; : _alphaPipeline!;
if (renderPass == WbRenderPass.Transparent) if (detailEnabled)
rangeBasePipeline = PipelineForMaterial(drawRange.MaterialState);
if (renderPass == WbRenderPass.Transparent || detailEnabled)
{ {
// Blend state is the pipeline's; switching variants mid-pass has // Blend state is the pipeline's; switching variants mid-pass has
// to re-establish the mesh, which is vertex-array state. // to re-establish the mesh, which is vertex-array state.
@ -289,8 +301,12 @@ public sealed unsafe partial class EnvCellRenderer
pushConstants.RenderPass = isAdditive pushConstants.RenderPass = isAdditive
? (int)renderPass | 0x100 ? (int)renderPass | 0x100
: (int)renderPass; : (int)renderPass;
if (detailEnabled && !drawRange.MaterialState.FogEnabled)
pushConstants.RenderPass |= RetailDetailTextureContract.NoFogRenderPassFlag;
pushConstants.DrawIdOffset = drawRange.FirstCommand; pushConstants.DrawIdOffset = drawRange.FirstCommand;
pushConstants.ParamB = isClip pushConstants.ParamB = detailEnabled
? drawRange.MaterialState.AlphaTestReference
: isClip
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f ? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
: 0f; : 0f;
pushConstants.TextureIndexA = detailEnabled pushConstants.TextureIndexA = detailEnabled
@ -311,9 +327,23 @@ public sealed unsafe partial class EnvCellRenderer
pushConstants.TextureIndexA = 0; pushConstants.TextureIndexA = 0;
pushConstants.ParamA = 0f; pushConstants.ParamA = 0f;
pushConstants.ParamB = 0f; pushConstants.ParamB = 0f;
pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag;
encoder.SetPushConstants(in pushConstants); encoder.SetPushConstants(in pushConstants);
} }
private IGpuPipeline PipelineForMaterial(RetailSetSurfaceMaterialState material) =>
material.Blend switch
{
RetailSetSurfaceBlend.Opaque => _opaquePipeline!,
RetailSetSurfaceBlend.StraightAlpha => _alphaPipeline!,
RetailSetSurfaceBlend.AlphaAdditive => _additivePipeline!,
RetailSetSurfaceBlend.Additive => _rawAdditivePipeline!,
RetailSetSurfaceBlend.InverseAlpha => _inversePipeline!,
RetailSetSurfaceBlend.InverseAdditive => _inverseAdditivePipeline!,
RetailSetSurfaceBlend.Clip => _clipPipeline!,
_ => throw new ArgumentOutOfRangeException(nameof(material), material, "Unknown SetSurface blend."),
};
private void BindPipelineWithMesh( private void BindPipelineWithMesh(
IGpuPassEncoder encoder, IGpuPassEncoder encoder,
IGpuPipeline pipeline, IGpuPipeline pipeline,
@ -429,5 +459,11 @@ public sealed unsafe partial class EnvCellRenderer
_clipPipeline = null; _clipPipeline = null;
_additivePipeline?.Dispose(); _additivePipeline?.Dispose();
_additivePipeline = null; _additivePipeline = null;
_rawAdditivePipeline?.Dispose();
_rawAdditivePipeline = null;
_inversePipeline?.Dispose();
_inversePipeline = null;
_inverseAdditivePipeline?.Dispose();
_inverseAdditivePipeline = null;
} }
} }

View file

@ -24,6 +24,7 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AcDream.Core.Meshing;
using DatReaderWriter.Enums; using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb; namespace AcDream.App.Rendering.Wb;
@ -162,7 +163,11 @@ public sealed partial class EnvCellRenderer :
private readonly List<MdiDrawRange> _mdiDrawRanges = new(); private readonly List<MdiDrawRange> _mdiDrawRanges = new();
private readonly record struct DrawCallRange(int First, int Count); private readonly record struct DrawCallRange(int First, int Count);
internal readonly record struct MdiDrawRange(int GroupIndex, int FirstCommand, int CommandCount); internal readonly record struct MdiDrawRange(
int GroupIndex,
int FirstCommand,
int CommandCount,
RetailSetSurfaceMaterialState MaterialState);
// Unfiltered rendering is retained for diagnostic callers only. Build its // Unfiltered rendering is retained for diagnostic callers only. Build its
// global grouping lazily instead of duplicating every prepared gameplay // global grouping lazily instead of duplicating every prepared gameplay
// instance in both a per-cell and global tree each frame. // instance in both a per-cell and global tree each frame.
@ -883,7 +888,7 @@ public sealed partial class EnvCellRenderer :
EnvCellTransparentRoute transparentRoute, EnvCellTransparentRoute transparentRoute,
bool detailSurfaceActive) bool detailSurfaceActive)
{ {
// WB EnvCellRenderManager.cs:400: the RHI arm's four pipelines are built // WB EnvCellRenderManager.cs:400: the RHI arm's SetSurface pipelines are built
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone // at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
// answers whether this renderer is ready to draw. // answers whether this renderer is ready to draw.
if (!_initialized) return; if (!_initialized) return;
@ -1304,7 +1309,6 @@ public sealed partial class EnvCellRenderer :
int groupIndex = _activeCullGroups[activeIndex]; int groupIndex = _activeCullGroups[activeIndex];
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group = List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
_batchesByCullGroup[groupIndex]; _batchesByCullGroup[groupIndex];
int firstCommand = cmdIndex;
foreach (var item in group) foreach (var item in group)
{ {
_modernBatches[cmdIndex] = new ModernBatchData _modernBatches[cmdIndex] = new ModernBatchData
@ -1330,22 +1334,14 @@ public sealed partial class EnvCellRenderer :
BaseVertex = (int)item.batch.BaseVertex, BaseVertex = (int)item.batch.BaseVertex,
BaseInstance = (uint)item.instanceOffset, BaseInstance = (uint)item.instanceOffset,
}; };
cmdIndex++;
}
int commandCount = cmdIndex - firstCommand;
if (commandCount == 0)
continue;
// Adjacent cells frequently resolve to the same cull/blend
// state. Their commands are already contiguous and remain in
// strict cell order, so one MDI call can cover the complete run
// without changing alpha compositing or gl_DrawID indexing.
AppendMdiDrawRange( AppendMdiDrawRange(
_mdiDrawRanges, _mdiDrawRanges,
groupIndex, groupIndex,
firstCommand, cmdIndex,
commandCount); 1,
item.batch.MaterialState);
cmdIndex++;
}
} }
} }
@ -1400,7 +1396,8 @@ public sealed partial class EnvCellRenderer :
List<MdiDrawRange> ranges, List<MdiDrawRange> ranges,
int groupIndex, int groupIndex,
int firstCommand, int firstCommand,
int commandCount) int commandCount,
RetailSetSurfaceMaterialState materialState)
{ {
ArgumentNullException.ThrowIfNull(ranges); ArgumentNullException.ThrowIfNull(ranges);
if (commandCount <= 0) if (commandCount <= 0)
@ -1410,6 +1407,7 @@ public sealed partial class EnvCellRenderer :
{ {
MdiDrawRange previous = ranges[^1]; MdiDrawRange previous = ranges[^1];
if (previous.GroupIndex == groupIndex if (previous.GroupIndex == groupIndex
&& previous.MaterialState == materialState
&& previous.FirstCommand + previous.CommandCount == firstCommand) && previous.FirstCommand + previous.CommandCount == firstCommand)
{ {
ranges[^1] = previous with ranges[^1] = previous with
@ -1420,7 +1418,7 @@ public sealed partial class EnvCellRenderer :
} }
} }
ranges.Add(new MdiDrawRange(groupIndex, firstCommand, commandCount)); ranges.Add(new MdiDrawRange(groupIndex, firstCommand, commandCount, materialState));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -51,6 +51,7 @@ internal readonly record struct GroupKey(
GpuTextureSlot TextureSlot, GpuTextureSlot TextureSlot,
uint TextureLayer, uint TextureLayer,
TranslucencyKind Translucency, TranslucencyKind Translucency,
RetailSetSurfaceMaterialState MaterialState,
uint FoliageFlags, uint FoliageFlags,
float SurfaceOpacity = 1f, float SurfaceOpacity = 1f,
CullMode CullMode = CullMode.CounterClockwise); CullMode CullMode = CullMode.CounterClockwise);

View file

@ -120,6 +120,8 @@ namespace AcDream.App.Rendering.Wb
public TextureKey Key { get; set; } public TextureKey Key { get; set; }
public DatReaderWriter.Enums.CullMode CullMode { get; set; } public DatReaderWriter.Enums.CullMode CullMode { get; set; }
public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; } public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; }
public AcDream.Core.Meshing.RetailSetSurfaceMaterialState MaterialState { get; set; }
= AcDream.Core.Meshing.RetailSetSurfaceMaterialState.Opaque;
public bool IsTransparent { get; set; } public bool IsTransparent { get; set; }
public bool IsAdditive { get; set; } public bool IsAdditive { get; set; }
public bool HasWrappingUVs { get; set; } public bool HasWrappingUVs { get; set; }
@ -2262,6 +2264,7 @@ namespace AcDream.App.Rendering.Wb
TextureFormat = format.Format, TextureFormat = format.Format,
RetailSurfaceMask = batch.RetailSurfaceMask, RetailSurfaceMask = batch.RetailSurfaceMask,
Translucency = batch.Translucency, Translucency = batch.Translucency,
MaterialState = batch.MaterialState,
IsTransparent = batch.IsTransparent, IsTransparent = batch.IsTransparent,
IsAdditive = batch.IsAdditive, IsAdditive = batch.IsAdditive,
HasWrappingUVs = batch.HasWrappingUVs, HasWrappingUVs = batch.HasWrappingUVs,

View file

@ -488,7 +488,14 @@ public sealed unsafe partial class WbDrawDispatcher
if (global is null) if (global is null)
return; return;
MeshPipelineSet pipelines = PipelinesFor(encoder); IGpuFrame frame = _orderedFrame
?? throw new InvalidOperationException(
"DrawOrderedRange has no frame to bind clip-region/"
+ "scene-lighting sections against — PrepareOrderedStream must run first.");
MeshPipelineSet pipelines = PipelinesFor(
encoder,
frame,
out DirectionalShadowFrameBinding shadowBinding);
var pushConstants = new GpuPushConstants var pushConstants = new GpuPushConstants
{ {
ViewProjection = _orderedViewProjection, ViewProjection = _orderedViewProjection,
@ -503,11 +510,6 @@ public sealed unsafe partial class WbDrawDispatcher
}; };
{ {
IGpuFrame frame = _orderedFrame
?? throw new InvalidOperationException(
"DrawOrderedRange has no frame to bind clip-region/"
+ "scene-lighting sections against — PrepareOrderedStream must run first.");
// Bind the SECTIONS on EVERY range call — never latch them // Bind the SECTIONS on EVERY range call — never latch them
// across calls. Between ordered ranges the walk's leaf draws run // across calls. Between ordered ranges the walk's leaf draws run
// (terrain, cell shells, sky, punch fans) and RetailAlphaQueue // (terrain, cell shells, sky, punch fans) and RetailAlphaQueue
@ -521,6 +523,7 @@ public sealed unsafe partial class WbDrawDispatcher
// DrawPreparedAlphaBatchRhi does for the same reason. // DrawPreparedAlphaBatchRhi does for the same reason.
BindPipelineWithMesh(encoder, pipelines.Opaque, global); BindPipelineWithMesh(encoder, pipelines.Opaque, global);
encoder.SetPushConstants(in pushConstants); encoder.SetPushConstants(in pushConstants);
BindDirectionalShadowReceiver(encoder, in shadowBinding);
BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances); BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances);
BindSection(encoder, GpuBindingModel.StorageBatches, _orderedBatches); BindSection(encoder, GpuBindingModel.StorageBatches, _orderedBatches);
BindSection(encoder, GpuBindingModel.StorageClipSlots, _orderedClipSlots); BindSection(encoder, GpuBindingModel.StorageClipSlots, _orderedClipSlots);
@ -567,11 +570,16 @@ public sealed unsafe partial class WbDrawDispatcher
ValidateMergeRun(_orderedStream, run); ValidateMergeRun(_orderedStream, run);
PipelineBucket bucket = BucketFor(_orderedStream.Keys[run.FirstCommand].Translucency); PipelineBucket bucket = BucketFor(_orderedStream.Keys[run.FirstCommand].Translucency);
IGpuPipeline pipeline = PipelineForBucket(pipelines, bucket); GroupKey key = _orderedStream.Keys[run.FirstCommand];
bool hasDetail = detailEnabled
&& _orderedStream.DetailCategories[run.FirstCommand] != 0u;
IGpuPipeline bucketPipeline = PipelineForBucket(pipelines, bucket);
IGpuPipeline pipeline = hasDetail
? PipelineForMaterial(pipelines, key.MaterialState, bucketPipeline)
: bucketPipeline;
pushConstants.RenderPass = bucket == PipelineBucket.Opaque ? 0 : 1; pushConstants.RenderPass = bucket == PipelineBucket.Opaque ? 0 : 1;
if (detailEnabled if (hasDetail)
&& _orderedStream.DetailCategories[run.FirstCommand] != 0u) ArmBuildingDetail(ref pushConstants, key.MaterialState);
ArmBuildingDetail(ref pushConstants);
else else
ClearDetailPushConstants(ref pushConstants); ClearDetailPushConstants(ref pushConstants);

View file

@ -52,7 +52,9 @@ public sealed unsafe partial class WbDrawDispatcher
IGpuPipeline OpaqueAlphaToCoverage, IGpuPipeline OpaqueAlphaToCoverage,
IGpuPipeline AlphaBlend, IGpuPipeline AlphaBlend,
IGpuPipeline AlphaAdditive, IGpuPipeline AlphaAdditive,
IGpuPipeline AlphaInverse); IGpuPipeline RawAdditive,
IGpuPipeline AlphaInverse,
IGpuPipeline InverseAdditive);
private MeshPipelineSet? _backbufferPipelines; private MeshPipelineSet? _backbufferPipelines;
private MeshPipelineSet? _offscreenPipelines; private MeshPipelineSet? _offscreenPipelines;
@ -227,7 +229,7 @@ public sealed unsafe partial class WbDrawDispatcher
bool usesRenderPackShaderAbi = false) bool usesRenderPackShaderAbi = false)
{ {
string suffix = samples > 1 ? string.Empty : "-1x"; string suffix = samples > 1 ? string.Empty : "-1x";
var created = new List<IGpuPipeline>(5); var created = new List<IGpuPipeline>(7);
try try
{ {
return new MeshPipelineSet( return new MeshPipelineSet(
@ -252,10 +254,20 @@ public sealed unsafe partial class WbDrawDispatcher
shaders: baseShaders, shaders: baseShaders,
shaderName: baseShaderName, shaderName: baseShaderName,
usesRenderPackShaderAbi: usesRenderPackShaderAbi)), usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
Track(CreateMeshPipeline(
device, $"{namePrefix}-raw-additive{suffix}", GpuBlendMode.RawAdditive, false, false, samples,
shaders: baseShaders,
shaderName: baseShaderName,
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
Track(CreateMeshPipeline( Track(CreateMeshPipeline(
device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples, device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples,
shaders: baseShaders, shaders: baseShaders,
shaderName: baseShaderName, shaderName: baseShaderName,
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
Track(CreateMeshPipeline(
device, $"{namePrefix}-inverse-additive{suffix}", GpuBlendMode.InverseAdditive, false, false, samples,
shaders: baseShaders,
shaderName: baseShaderName,
usesRenderPackShaderAbi: usesRenderPackShaderAbi))); usesRenderPackShaderAbi: usesRenderPackShaderAbi)));
} }
catch catch
@ -370,9 +382,12 @@ public sealed unsafe partial class WbDrawDispatcher
encoder, encoder,
frame, frame,
out DirectionalShadowFrameBinding shadowBinding); out DirectionalShadowFrameBinding shadowBinding);
IGpuPipeline opaquePipeline = AlphaToCoverage
? pipelines.OpaqueAlphaToCoverage
: pipelines.Opaque;
BindPipelineWithMesh( BindPipelineWithMesh(
encoder, encoder,
AlphaToCoverage ? pipelines.OpaqueAlphaToCoverage : pipelines.Opaque, opaquePipeline,
mesh); mesh);
encoder.SetPushConstants(in pushConstants); encoder.SetPushConstants(in pushConstants);
BindDirectionalShadowReceiver(encoder, in shadowBinding); BindDirectionalShadowReceiver(encoder, in shadowBinding);
@ -436,7 +451,8 @@ public sealed unsafe partial class WbDrawDispatcher
using (BeginRhiTimer(encoder, diag, OpaqueTimerScope)) using (BeginRhiTimer(encoder, diag, OpaqueTimerScope))
{ {
DrawDetailAwareRangeRhi( DrawDetailAwareRangeRhi(
encoder, ref pushConstants, commandBuffer, commandBase, encoder, mesh, pipelines, opaquePipeline,
ref pushConstants, commandBuffer, commandBase,
0, _opaqueDrawCount, usedDetailCategories, detailEnabled); 0, _opaqueDrawCount, usedDetailCategories, detailEnabled);
} }
} }
@ -634,7 +650,10 @@ public sealed unsafe partial class WbDrawDispatcher
/// (M3c) precisely because detail-eligible content no longer reaches it. /// (M3c) precisely because detail-eligible content no longer reaches it.
/// </summary> /// </summary>
private void DrawImmediateAlphaInstanceRhi( private void DrawImmediateAlphaInstanceRhi(
GlobalMeshBuffer mesh, TranslucencyKind blend, Matrix4x4 viewProjection) GlobalMeshBuffer mesh,
TranslucencyKind blend,
RetailSetSurfaceMaterialState materialState,
Matrix4x4 viewProjection)
{ {
if (_alphaCommands.Buffer is null) if (_alphaCommands.Buffer is null)
return; return;
@ -644,9 +663,12 @@ public sealed unsafe partial class WbDrawDispatcher
GpuPushConstants pushConstants = BindAlphaDrawState( GpuPushConstants pushConstants = BindAlphaDrawState(
encoder, frame, mesh, viewProjection, out MeshPipelineSet pipelines); encoder, frame, mesh, viewProjection, out MeshPipelineSet pipelines);
BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh); BindPipelineWithMesh(
encoder,
PipelineForMaterial(pipelines, materialState, pipelines.Opaque),
mesh);
encoder.SetPushConstants(in pushConstants); encoder.SetPushConstants(in pushConstants);
ArmBuildingDetail(ref pushConstants); ArmBuildingDetail(ref pushConstants, materialState);
DrawIndirectRangeRhi( DrawIndirectRangeRhi(
encoder, encoder,
ref pushConstants, ref pushConstants,
@ -673,6 +695,8 @@ public sealed unsafe partial class WbDrawDispatcher
while (command < end) while (command < end)
{ {
TranslucencyKind blend = _groupInputScratch[command].Translucency; TranslucencyKind blend = _groupInputScratch[command].Translucency;
RetailSetSurfaceMaterialState materialState =
_groupInputScratch[command].MaterialState;
bool hasDetail = detailEnabled bool hasDetail = detailEnabled
&& CommandContainsDetailCategory( && CommandContainsDetailCategory(
_indirectCommands[command], _indirectCommands[command],
@ -691,10 +715,12 @@ public sealed unsafe partial class WbDrawDispatcher
BindPipelineWithMesh( BindPipelineWithMesh(
encoder, encoder,
PipelineForBlend(pipelines, blend), hasDetail
? PipelineForMaterial(pipelines, materialState, pipelines.Opaque)
: PipelineForBlend(pipelines, blend),
mesh); mesh);
if (hasDetail) if (hasDetail)
ArmBuildingDetail(ref pushConstants); ArmBuildingDetail(ref pushConstants, materialState);
else else
ClearDetailPushConstants(ref pushConstants); ClearDetailPushConstants(ref pushConstants);
DrawIndirectRangeRhi( DrawIndirectRangeRhi(
@ -712,6 +738,9 @@ public sealed unsafe partial class WbDrawDispatcher
private void DrawDetailAwareRangeRhi( private void DrawDetailAwareRangeRhi(
IGpuPassEncoder encoder, IGpuPassEncoder encoder,
GlobalMeshBuffer mesh,
MeshPipelineSet pipelines,
IGpuPipeline opaquePipeline,
ref GpuPushConstants pushConstants, ref GpuPushConstants pushConstants,
IGpuBuffer commandBuffer, IGpuBuffer commandBuffer,
uint commandBase, uint commandBase,
@ -738,9 +767,20 @@ public sealed unsafe partial class WbDrawDispatcher
} }
if (hasDetail) if (hasDetail)
ArmBuildingDetail(ref pushConstants); {
RetailSetSurfaceMaterialState materialState =
_groupInputScratch[command].MaterialState;
BindPipelineWithMesh(
encoder,
PipelineForMaterial(pipelines, materialState, opaquePipeline),
mesh);
ArmBuildingDetail(ref pushConstants, materialState);
}
else else
{
BindPipelineWithMesh(encoder, opaquePipeline, mesh);
ClearDetailPushConstants(ref pushConstants); ClearDetailPushConstants(ref pushConstants);
}
DrawIndirectRangeRhi( DrawIndirectRangeRhi(
encoder, encoder,
ref pushConstants, ref pushConstants,
@ -754,10 +794,17 @@ public sealed unsafe partial class WbDrawDispatcher
encoder.SetPushConstants(in pushConstants); encoder.SetPushConstants(in pushConstants);
} }
private void ArmBuildingDetail(ref GpuPushConstants pushConstants) private void ArmBuildingDetail(
ref GpuPushConstants pushConstants,
RetailSetSurfaceMaterialState materialState)
{ {
pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index; pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index;
pushConstants.ParamA = _buildingDetail.Tiling; pushConstants.ParamA = _buildingDetail.Tiling;
pushConstants.ParamB = materialState.AlphaTestReference;
if (materialState.FogEnabled)
pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag;
else
pushConstants.RenderPass |= RetailDetailTextureContract.NoFogRenderPassFlag;
} }
private static void ClearDetailPushConstants( private static void ClearDetailPushConstants(
@ -766,6 +813,7 @@ public sealed unsafe partial class WbDrawDispatcher
pushConstants.TextureIndexA = 0; pushConstants.TextureIndexA = 0;
pushConstants.ParamA = 0f; pushConstants.ParamA = 0f;
pushConstants.ParamB = 0f; pushConstants.ParamB = 0f;
pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag;
} }
private static IGpuPipeline PipelineForBlend(MeshPipelineSet pipelines, TranslucencyKind blend) => private static IGpuPipeline PipelineForBlend(MeshPipelineSet pipelines, TranslucencyKind blend) =>
@ -776,6 +824,23 @@ public sealed unsafe partial class WbDrawDispatcher
_ => pipelines.AlphaBlend, _ => pipelines.AlphaBlend,
}; };
private static IGpuPipeline PipelineForMaterial(
MeshPipelineSet pipelines,
RetailSetSurfaceMaterialState material,
IGpuPipeline opaquePipeline) => material.Blend switch
{
RetailSetSurfaceBlend.Opaque => opaquePipeline,
RetailSetSurfaceBlend.StraightAlpha => pipelines.AlphaBlend,
RetailSetSurfaceBlend.AlphaAdditive => pipelines.AlphaAdditive,
RetailSetSurfaceBlend.Additive => pipelines.RawAdditive,
RetailSetSurfaceBlend.InverseAlpha => pipelines.AlphaInverse,
RetailSetSurfaceBlend.InverseAdditive => pipelines.InverseAdditive,
// AP-240: ordinary Gfx/building pure Clip stays on its selected
// opaque/A2C pipeline; exact final-X ParamB still reaches shader.
RetailSetSurfaceBlend.Clip => opaquePipeline,
_ => throw new ArgumentOutOfRangeException(nameof(material), material, "Unknown SetSurface blend."),
};
/// <summary> /// <summary>
/// Reads cull modes from <paramref name="cullModes"/> when the caller /// Reads cull modes from <paramref name="cullModes"/> when the caller
/// supplies one, or from the shared <see cref="_drawCullModes"/> scratch /// supplies one, or from the shared <see cref="_drawCullModes"/> scratch
@ -1102,7 +1167,9 @@ public sealed unsafe partial class WbDrawDispatcher
pipelines.OpaqueAlphaToCoverage.Dispose(); pipelines.OpaqueAlphaToCoverage.Dispose();
pipelines.AlphaBlend.Dispose(); pipelines.AlphaBlend.Dispose();
pipelines.AlphaAdditive.Dispose(); pipelines.AlphaAdditive.Dispose();
pipelines.RawAdditive.Dispose();
pipelines.AlphaInverse.Dispose(); pipelines.AlphaInverse.Dispose();
pipelines.InverseAdditive.Dispose();
} }
private sealed class NullRhiTimerScope : IDisposable private sealed class NullRhiTimerScope : IDisposable

View file

@ -240,6 +240,7 @@ public sealed partial class WbDrawDispatcher
key = new GroupKey( key = new GroupKey(
batch.FirstIndex, (int)batch.BaseVertex, batch.FirstIndex, (int)batch.BaseVertex,
batch.IndexCount, texture.Slot, texture.Layer, translucency, batch.IndexCount, texture.Slot, texture.Layer, translucency,
MaterialState: batch.MaterialState,
FoliageFlags: foliageFlags, FoliageFlags: foliageFlags,
SurfaceOpacity: batch.SurfaceOpacity, SurfaceOpacity: batch.SurfaceOpacity,
CullMode: batch.CullMode); CullMode: batch.CullMode);

View file

@ -2050,6 +2050,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
TextureIndex: g.TextureSlot.Index, TextureIndex: g.TextureSlot.Index,
TextureLayer: g.TextureLayer, TextureLayer: g.TextureLayer,
Translucency: g.Translucency, Translucency: g.Translucency,
MaterialState: g.MaterialState,
SurfaceOpacity: g.SurfaceOpacity, SurfaceOpacity: g.SurfaceOpacity,
CullMode: g.CullMode, CullMode: g.CullMode,
FoliageFlags: g.FoliageFlags); FoliageFlags: g.FoliageFlags);
@ -2123,6 +2124,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
g.TextureSlot, g.TextureSlot,
g.TextureLayer, g.TextureLayer,
g.Translucency, g.Translucency,
g.MaterialState,
g.FoliageFlags, g.FoliageFlags,
g.SurfaceOpacity, g.SurfaceOpacity,
g.CullMode); g.CullMode);
@ -2144,6 +2146,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
TextureSlot = key.TextureSlot, TextureSlot = key.TextureSlot,
TextureLayer = key.TextureLayer, TextureLayer = key.TextureLayer,
Translucency = key.Translucency, Translucency = key.Translucency,
MaterialState = key.MaterialState,
CullMode = key.CullMode, CullMode = key.CullMode,
FoliageFlags = key.FoliageFlags, FoliageFlags = key.FoliageFlags,
SurfaceOpacity = key.SurfaceOpacity, SurfaceOpacity = key.SurfaceOpacity,
@ -2641,7 +2644,11 @@ public sealed partial class WbDrawDispatcher : IDisposable
EnsureDeferredAlphaCapacity(1); EnsureDeferredAlphaCapacity(1);
WriteDeferredAlphaEntrySlot(0, in entry); WriteDeferredAlphaEntrySlot(0, in entry);
PrepareRhiAlphaSections(1); PrepareRhiAlphaSections(1);
DrawImmediateAlphaInstanceRhi(global, entry.Key.Translucency, viewProjection); DrawImmediateAlphaInstanceRhi(
global,
entry.Key.Translucency,
entry.Key.MaterialState,
viewProjection);
} }
private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
@ -3437,6 +3444,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
uint TextureIndex, uint TextureIndex,
uint TextureLayer, uint TextureLayer,
TranslucencyKind Translucency, TranslucencyKind Translucency,
RetailSetSurfaceMaterialState MaterialState,
float SurfaceOpacity = 1f, float SurfaceOpacity = 1f,
CullMode CullMode = CullMode.CounterClockwise, CullMode CullMode = CullMode.CounterClockwise,
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag / // Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
@ -3675,6 +3683,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned; AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
public uint TextureLayer; // Layer in either the pooled composite array or WB shared atlas. public uint TextureLayer; // Layer in either the pooled composite array or WB shared atlas.
public TranslucencyKind Translucency; public TranslucencyKind Translucency;
public RetailSetSurfaceMaterialState MaterialState =
RetailSetSurfaceMaterialState.Opaque;
public float SurfaceOpacity = 1f; public float SurfaceOpacity = 1f;
public CullMode CullMode; public CullMode CullMode;
public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes) public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes)

View file

@ -396,6 +396,7 @@ public sealed class MeshExtractor {
bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type); bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type);
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap); bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
uint paletteId = 0; uint paletteId = 0;
bool texturePresent = false;
bool isDxt3or5 = false; bool isDxt3or5 = false;
bool textureDataIsCached = false; bool textureDataIsCached = false;
DatReaderWriter.Enums.PixelFormat? sourceFormat = null; DatReaderWriter.Enums.PixelFormat? sourceFormat = null;
@ -410,6 +411,7 @@ public sealed class MeshExtractor {
textureDataIsCached = true; textureDataIsCached = true;
} }
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) { else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
texturePresent = true;
var renderSurfaceId = surfaceTexture.Textures.First(); var renderSurfaceId = surfaceTexture.Textures.First();
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var renderSurface)) { if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var renderSurface)) {
// check highres // check highres
@ -603,6 +605,10 @@ public sealed class MeshExtractor {
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency( TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
surface.Type, surface.Type,
surface.Translucency), surface.Translucency),
MaterialState = RetailSetSurfaceMaterialState.Resolve(
surface.Type,
texturePresent,
paletteId != 0),
IsTransparent = isTransparent, IsTransparent = isTransparent,
IsAdditive = isAdditive IsAdditive = isAdditive
}; };
@ -900,6 +906,7 @@ public sealed class MeshExtractor {
bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type); bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type);
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap); bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
uint paletteId = 0; uint paletteId = 0;
bool texturePresent = false;
bool isDxt3or5 = false; bool isDxt3or5 = false;
bool textureDataIsCached = false; bool textureDataIsCached = false;
DatReaderWriter.Enums.PixelFormat? sourceFormat = null; DatReaderWriter.Enums.PixelFormat? sourceFormat = null;
@ -914,6 +921,7 @@ public sealed class MeshExtractor {
textureDataIsCached = true; textureDataIsCached = true;
} }
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) { else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
texturePresent = true;
var renderSurfaceId = surfaceTexture.Textures.First(); var renderSurfaceId = surfaceTexture.Textures.First();
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var renderSurface)) { if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var renderSurface)) {
if (!_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out var hrRenderSurface)) { if (!_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out var hrRenderSurface)) {
@ -1080,6 +1088,10 @@ public sealed class MeshExtractor {
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency( TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
surface.Type, surface.Type,
surface.Translucency), surface.Translucency),
MaterialState = RetailSetSurfaceMaterialState.Resolve(
surface.Type,
texturePresent,
paletteId != 0),
IsTransparent = isTransparent, IsTransparent = isTransparent,
IsAdditive = isAdditive, IsAdditive = isAdditive,
}; };

View file

@ -181,6 +181,13 @@ public class TextureBatchData {
/// </summary> /// </summary>
public float SurfaceOpacity { get; set; } = 1f; public float SurfaceOpacity { get; set; } = 1f;
/// <summary>
/// Exact retail SetSurface blend/alpha-test/fog state resolved during
/// extraction from the source Surface and texture palette class.
/// </summary>
public RetailSetSurfaceMaterialState MaterialState { get; set; } =
RetailSetSurfaceMaterialState.Opaque;
/// <summary> /// <summary>
/// Retail source surface-array index this subset was constructed from /// Retail source surface-array index this subset was constructed from
/// (contract §3.6) — the CellStruct subset/material OWNER, not the /// (contract §3.6) — the CellStruct subset/material OWNER, not the

View file

@ -228,9 +228,13 @@ public static class ObjectMeshDataSerializer {
w.Write(batch.RetailSurfaceMask); w.Write(batch.RetailSurfaceMask);
w.Write(batch.RawSurfaceType); w.Write(batch.RawSurfaceType);
w.Write(batch.IsCellShell); w.Write(batch.IsCellShell);
// OVERHAUL S5-c4: recipe 9 appends authored material opacity. Recipe // OVERHAUL S5-c4: recipe 9 appended authored material opacity. Recipe
// 10 appends the extraction-resolved SetSurface state byte. Recipe
// identity rejects older payloads before this serializer is entered. // identity rejects older payloads before this serializer is entered.
w.Write(batch.SurfaceOpacity); w.Write(batch.SurfaceOpacity);
// S5-c4 fix round 1: recipe 10 appends the immutable resolved
// SetSurface state as one validated deterministic byte.
w.Write(batch.MaterialState.ToPackedByte());
} }
private static TextureBatchData ReadTextureBatchData( private static TextureBatchData ReadTextureBatchData(
@ -256,6 +260,9 @@ public static class ObjectMeshDataSerializer {
batch.RawSurfaceType = r.ReadUInt32(); batch.RawSurfaceType = r.ReadUInt32();
batch.IsCellShell = r.ReadBoolean(); batch.IsCellShell = r.ReadBoolean();
batch.SurfaceOpacity = r.ReadSingle(); batch.SurfaceOpacity = r.ReadSingle();
batch.MaterialState =
AcDream.Core.Meshing.RetailSetSurfaceMaterialState.FromPackedByte(
r.ReadByte());
return batch; return batch;
} }

View file

@ -45,9 +45,11 @@ public static class PakFormat {
/// (Surface.Type &amp; (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after /// (Surface.Type &amp; (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after
/// surface resolution. Version 9 appends authored surface opacity to /// surface resolution. Version 9 appends authored surface opacity to
/// every prepared mesh texture batch. The binary format remains version /// every prepared mesh texture batch. The binary format remains version
/// 2, but every prepared render record must be regenerated. /// 2, but every prepared render record must be regenerated. Version 10
/// appends the exact resolved SetSurface blend, alpha-test, and fog state
/// to every prepared texture batch; recipe 9 cannot be read as recipe 10.
/// </summary> /// </summary>
public const uint CurrentBakeToolVersion = 9; public const uint CurrentBakeToolVersion = 10;
} }
/// <summary> /// <summary>

View file

@ -38,6 +38,135 @@ public enum TranslucencyKind
InvAlpha = 4, InvAlpha = 4,
} }
/// <summary>
/// Exact framebuffer blend selected by retail's
/// <c>D3DPolyRender::SetSurface</c>. This is deliberately separate from
/// <see cref="TranslucencyKind"/>, which remains the existing queue-membership
/// classification and cannot distinguish the additive factor pairs.
/// </summary>
public enum RetailSetSurfaceBlend : byte
{
Opaque = 0,
StraightAlpha = 1,
AlphaAdditive = 2,
Additive = 3,
InverseAlpha = 4,
InverseAdditive = 5,
Clip = 6,
}
/// <summary>The exact final-alpha comparison selected by SetSurface.</summary>
public enum RetailSetSurfaceAlphaTest : byte
{
Disabled = 0,
Paletted = 1,
Dds = 2,
}
/// <summary>
/// Immutable resolved SetSurface state prepared from the source Surface and
/// its actual texture/palette class. Renderers consume this value directly;
/// they never reconstruct it from queue kind or texture identity.
/// </summary>
public readonly record struct RetailSetSurfaceMaterialState(
RetailSetSurfaceBlend Blend,
RetailSetSurfaceAlphaTest AlphaTest,
bool FogEnabled)
{
public static RetailSetSurfaceMaterialState Opaque { get; } = new(
RetailSetSurfaceBlend.Opaque,
RetailSetSurfaceAlphaTest.Disabled,
FogEnabled: true);
public bool AlphaTestEnabled => AlphaTest != RetailSetSurfaceAlphaTest.Disabled;
public float AlphaTestReference => AlphaTest switch
{
RetailSetSurfaceAlphaTest.Disabled => 0f,
RetailSetSurfaceAlphaTest.Paletted => 100f / 255f,
RetailSetSurfaceAlphaTest.Dds => 200f / 255f,
_ => throw new ArgumentOutOfRangeException(nameof(AlphaTest), AlphaTest, null),
};
/// <summary>One deterministic recipe byte; bits 6-7 remain reserved.</summary>
public byte ToPackedByte()
{
if ((uint)Blend > (uint)RetailSetSurfaceBlend.Clip)
throw new InvalidOperationException($"Unknown SetSurface blend {Blend}.");
if ((uint)AlphaTest > (uint)RetailSetSurfaceAlphaTest.Dds)
throw new InvalidOperationException($"Unknown SetSurface alpha test {AlphaTest}.");
return (byte)((byte)Blend | ((byte)AlphaTest << 3) | (FogEnabled ? 0 : 0x20));
}
public static RetailSetSurfaceMaterialState FromPackedByte(byte value)
{
if ((value & 0xC0) != 0)
throw new InvalidDataException($"Reserved SetSurface state bits are set: 0x{value:X2}.");
var blend = (RetailSetSurfaceBlend)(value & 0x07);
var alphaTest = (RetailSetSurfaceAlphaTest)((value >> 3) & 0x03);
if ((uint)blend > (uint)RetailSetSurfaceBlend.Clip
|| (uint)alphaTest > (uint)RetailSetSurfaceAlphaTest.Dds)
{
throw new InvalidDataException($"Invalid SetSurface state byte: 0x{value:X2}.");
}
return new RetailSetSurfaceMaterialState(
blend,
alphaTest,
FogEnabled: (value & 0x20) == 0);
}
public static RetailSetSurfaceMaterialState Resolve(
SurfaceType type,
bool texturePresent,
bool textureHasPalette)
{
bool additive = (type & SurfaceType.Additive) != 0;
bool alpha = (type & SurfaceType.Alpha) != 0;
bool inverse = (type & SurfaceType.InvAlpha) != 0;
bool clip = (type & SurfaceType.Base1ClipMap) != 0;
bool translucent = (type & SurfaceType.Translucent) != 0;
RetailSetSurfaceBlend blend = alpha
? additive
? RetailSetSurfaceBlend.AlphaAdditive
: RetailSetSurfaceBlend.StraightAlpha
: inverse
? additive
? RetailSetSurfaceBlend.InverseAdditive
: RetailSetSurfaceBlend.InverseAlpha
: additive
? RetailSetSurfaceBlend.Additive
: RetailSetSurfaceBlend.Opaque;
RetailSetSurfaceAlphaTest alphaTest = RetailSetSurfaceAlphaTest.Disabled;
if (clip)
{
alphaTest = texturePresent && textureHasPalette
? RetailSetSurfaceAlphaTest.Paletted
: RetailSetSurfaceAlphaTest.Dds;
if (blend == RetailSetSurfaceBlend.Opaque)
blend = RetailSetSurfaceBlend.Clip;
}
// Retail's later Translucent arm forces every ClipMap combination and
// the otherwise-opaque/straight cases to straight alpha, disabling the
// ClipMap test. Established additive/inverse families remain intact.
if (translucent
&& (clip
|| blend is RetailSetSurfaceBlend.Opaque
or RetailSetSurfaceBlend.StraightAlpha))
{
blend = RetailSetSurfaceBlend.StraightAlpha;
alphaTest = RetailSetSurfaceAlphaTest.Disabled;
}
return new RetailSetSurfaceMaterialState(
blend,
alphaTest,
FogEnabled: !additive);
}
}
public static class TranslucencyKindExtensions public static class TranslucencyKindExtensions
{ {
// Translucent override comes FIRST, then the existing priority chain: // Translucent override comes FIRST, then the existing priority chain:

View file

@ -61,6 +61,10 @@ public static class ContentMigrationCatalog
8, 8,
9, 9,
"authored surface opacity in prepared mesh batches"), "authored surface opacity in prepared mesh batches"),
[10] = FullRebuild(
9,
10,
"exact resolved SetSurface state in prepared mesh batches"),
}; };
public static ContentMigrationPlan Resolve(uint fromRecipeVersion, uint targetRecipeVersion) public static ContentMigrationPlan Resolve(uint fromRecipeVersion, uint targetRecipeVersion)

View file

@ -39,9 +39,10 @@ public sealed class LauncherInstallRecordStore
// with retail's authored DrawingBSP view sphere. Version 8 (OH2/S1) keeps // with retail's authored DrawingBSP view sphere. Version 8 (OH2/S1) keeps
// pak format 2 and regenerates every CellStruct/EnvCell render record // pak format 2 and regenerates every CellStruct/EnvCell render record
// with retail's exact surface-array-index subset construction. Version 9 // with retail's exact surface-array-index subset construction. Version 9
// carries authored surface opacity in prepared mesh batches. Each is a // carries authored surface opacity in prepared mesh batches. Version 10
// mandatory full rebuild from its predecessor recipe. // adds exact resolved SetSurface state. Each is a mandatory full rebuild
public const uint CurrentBakeToolVersion = 9; // from its predecessor recipe.
public const uint CurrentBakeToolVersion = 10;
private static readonly JsonSerializerOptions SerializerOptions = new() private static readonly JsonSerializerOptions SerializerOptions = new()
{ {

View file

@ -8,7 +8,9 @@ using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content; using AcDream.Content;
using AcDream.Core.Meshing;
using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Enums;
using DatReaderWriter.Enums;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using CullMode = DatReaderWriter.Enums.CullMode; using CullMode = DatReaderWriter.Enums.CullMode;
@ -21,6 +23,11 @@ namespace AcDream.App.Tests.Rendering;
/// </summary> /// </summary>
public sealed class EnvCellAlphaDrawSourceTests public sealed class EnvCellAlphaDrawSourceTests
{ {
private static readonly Vector4 MaterialBase = new(0.31f, 0.57f, 0.83f, 0.19f);
private static readonly Vector3 MaterialDiffuse = new(0.73f, 0.41f, 0.67f);
private static readonly Vector4 MaterialDetail = new(0.91f, 0.23f, 0.49f, 0.62f);
private static readonly Vector4 MaterialDestination = new(0.17f, 0.37f, 0.71f, 0.29f);
/// <summary> /// <summary>
/// Canonical F4180104 surface 08000BFF's pure Base1ClipMap mask 0x08 /// Canonical F4180104 surface 08000BFF's pure Base1ClipMap mask 0x08
/// reaches CLIP while alpha-family 0x02 reaches ALPHA. Mutation check: /// reaches CLIP while alpha-family 0x02 reaches ALPHA. Mutation check:
@ -75,26 +82,45 @@ public sealed class EnvCellAlphaDrawSourceTests
DrawPipelineNames(fixture.Device)); DrawPipelineNames(fixture.Device));
} }
[Theory] public static TheoryData<SurfaceType, bool, string, object, float, bool> ExactMaterialRows() => new()
[InlineData(0, "envcell-opaque", 0f)]
[InlineData(1, "envcell-alpha", 0f)]
[InlineData(2, "envcell-additive", 0f)]
[InlineData(3, "envcell-clip", 200f / 255f)]
[InlineData(4, "envcell-clip", 100f / 255f)]
public void DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity(
int specIndex,
string expectedPipeline,
float expectedReference)
{ {
BatchSpec spec = specIndex switch { SurfaceType.Base1Image, false, "envcell-opaque", GpuBlendMode.None, 0f, true },
{ { SurfaceType.Base1Image | SurfaceType.Alpha, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, true },
0 => BatchSpec.Opaque, { SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false, "envcell-additive", GpuBlendMode.Additive, 0f, false },
1 => BatchSpec.Alpha, { SurfaceType.Base1Image | SurfaceType.Additive, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 0f, false },
2 => BatchSpec.Additive, { SurfaceType.Base1Image | SurfaceType.InvAlpha, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 0f, true },
3 => BatchSpec.ClipDds, { SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive, false, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 0f, false },
4 => BatchSpec.ClipPaletted, { SurfaceType.Translucent, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, true },
_ => throw new ArgumentOutOfRangeException(nameof(specIndex)), { SurfaceType.Translucent | SurfaceType.Additive, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 0f, false },
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 0f, true },
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, false, "envcell-clip", GpuBlendMode.PremultipliedAlpha, 200f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, true, "envcell-clip", GpuBlendMode.PremultipliedAlpha, 100f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 200f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "envcell-alpha", GpuBlendMode.StraightAlpha, 100f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-additive", GpuBlendMode.Additive, 200f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-additive", GpuBlendMode.Additive, 100f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 200f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-raw-additive", GpuBlendMode.RawAdditive, 100f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 200f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, true, "envcell-inverse", GpuBlendMode.InverseAlpha, 100f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 200f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 100f / 255f, false },
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, false },
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, false },
}; };
[Theory]
[MemberData(nameof(ExactMaterialRows))]
public void DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity(
SurfaceType surfaceType,
bool paletted,
string expectedPipeline,
object expectedBlendValue,
float expectedReference,
bool expectedFog)
{
var expectedBlend = (GpuBlendMode)expectedBlendValue;
BatchSpec spec = new(surfaceType, paletted, PositiveStippling: false);
using var fixture = new ProductionEnvCellFixture( using var fixture = new ProductionEnvCellFixture(
detailSurfaceActive: true, detailSurfaceActive: true,
spec); spec);
@ -113,6 +139,20 @@ public sealed class EnvCellAlphaDrawSourceTests
DrawPushConstants(fixture.Device), DrawPushConstants(fixture.Device),
constants => constants.ParamA != 0f); constants => constants.ParamA != 0f);
Assert.NotEqual(0u, armed.TextureIndexA); Assert.NotEqual(0u, armed.TextureIndexA);
Assert.Equal(
!expectedFog,
(armed.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag) != 0);
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
surfaceType, texturePresent: true, textureHasPalette: paletted);
GpuPipelineDescription selected = fixture.Device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == expectedPipeline)
.Description;
Assert.Equal(expectedBlend, selected.Blend);
Assert.True(selected.Depth.Test);
Assert.Equal(
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
selected.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, selected.Depth.Compare);
Assert.Equal(0, fixture.Queue.PendingCount); Assert.Equal(0, fixture.Queue.PendingCount);
GpuRecordedStorageBind batchBind = fixture.Device.Calls GpuRecordedStorageBind batchBind = fixture.Device.Calls
.OfType<GpuRecordedStorageBind>() .OfType<GpuRecordedStorageBind>()
@ -120,10 +160,77 @@ public sealed class EnvCellAlphaDrawSourceTests
ReadOnlySpan<ModernBatchData> gpuBatches = MemoryMarshal.Cast<byte, ModernBatchData>( ReadOnlySpan<ModernBatchData> gpuBatches = MemoryMarshal.Cast<byte, ModernBatchData>(
fixture.Device.RingBytes.Slice((int)batchBind.OffsetBytes, (int)batchBind.SizeBytes)); fixture.Device.RingBytes.Slice((int)batchBind.OffsetBytes, (int)batchBind.SizeBytes));
Assert.Equal(0.25f, Assert.Single(gpuBatches.ToArray()).SurfaceOpacity); Assert.Equal(0.25f, Assert.Single(gpuBatches.ToArray()).SurfaceOpacity);
Vector4 source = RetailDetailTextureContract.Combine(
MaterialBase, MaterialDiffuse, MaterialDetail, 0.75f, 0.4f);
AssertVector(new Vector4(0.3534682f, 0.2330118f, 0.5438054f, 0.11532f), source);
RetailDetailTextureContract.FramebufferFamily family = expectedBlend switch
{
GpuBlendMode.None => RetailDetailTextureContract.FramebufferFamily.Opaque,
GpuBlendMode.StraightAlpha => RetailDetailTextureContract.FramebufferFamily.Alpha,
GpuBlendMode.Additive => RetailDetailTextureContract.FramebufferFamily.AlphaAdditive,
GpuBlendMode.RawAdditive => RetailDetailTextureContract.FramebufferFamily.Additive,
GpuBlendMode.InverseAlpha => RetailDetailTextureContract.FramebufferFamily.InverseAlpha,
GpuBlendMode.InverseAdditive => RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive,
GpuBlendMode.PremultipliedAlpha => RetailDetailTextureContract.FramebufferFamily.Clip,
_ => throw new ArgumentOutOfRangeException(nameof(expectedBlend)),
};
AssertVector(
IndependentComposite(source, MaterialDestination, family),
RetailDetailTextureContract.Composite(source, MaterialDestination, family));
if (resolved.AlphaTestEnabled)
{
Assert.False(RetailDetailTextureContract.SurvivesClip(
MathF.BitDecrement(expectedReference), expectedReference));
Assert.True(RetailDetailTextureContract.SurvivesClip(expectedReference, expectedReference));
Assert.True(RetailDetailTextureContract.SurvivesClip(
MathF.BitIncrement(expectedReference), expectedReference));
}
fixture.Queue.EndFrame(); fixture.Queue.EndFrame();
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()); Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
} }
public static TheoryData<SurfaceType, bool, string, float> DetailOffRows() => new()
{
{ SurfaceType.Base1Image, false, "envcell-opaque", 0f },
{ SurfaceType.Alpha, false, "envcell-alpha", 0f },
{ SurfaceType.Alpha | SurfaceType.Additive, false, "envcell-additive", 0f },
{ SurfaceType.Additive, false, "envcell-additive", 0f },
{ SurfaceType.InvAlpha, false, "envcell-alpha", 0f },
{ SurfaceType.InvAlpha | SurfaceType.Additive, false, "envcell-additive", 0f },
{ SurfaceType.Base1ClipMap, false, "envcell-clip", 200f / 255f },
{ SurfaceType.Base1ClipMap, true, "envcell-clip", 100f / 255f },
{ SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "envcell-alpha", 0f },
{ SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "envcell-alpha", 0f },
{ SurfaceType.Translucent, false, "envcell-alpha", 0f },
{ SurfaceType.Translucent | SurfaceType.Additive, false, "envcell-additive", 0f },
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, "envcell-alpha", 0f },
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, "envcell-additive", 0f },
};
[Theory]
[MemberData(nameof(DetailOffRows))]
public void DetailOff_EveryRawStateRetainsThePreFixLogicalPath(
SurfaceType surfaceType,
bool paletted,
string expectedPipeline,
float expectedReference)
{
using var fixture = new ProductionEnvCellFixture(
detailSurfaceActive: false,
new BatchSpec(surfaceType, paletted, PositiveStippling: false));
fixture.Queue.BeginFrame();
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
fixture.Queue.EndFrame();
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Equal([expectedPipeline], DrawPipelineNames(fixture.Device));
GpuPushConstants constants = Assert.Single(DrawPushConstants(fixture.Device));
Assert.Equal(0u, constants.TextureIndexA);
Assert.Equal(0f, constants.ParamA);
Assert.Equal(expectedReference, constants.ParamB);
Assert.Equal(0, constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
}
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>, /// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
/// with an unrelated source's entry appended between them, must still /// with an unrelated source's entry appended between them, must still
/// produce TWO separate single-cell draw calls around the interposed /// produce TWO separate single-cell draw calls around the interposed
@ -258,9 +365,10 @@ public sealed class EnvCellAlphaDrawSourceTests
foreach (string shader in new[] { modern, atmospheric }) foreach (string shader in new[] { modern, atmospheric })
{ {
Assert.Contains( Assert.Contains(
"if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)", "? isRetailClipReference(uParamB) && alpha < uParamB",
shader, shader,
StringComparison.Ordinal); StringComparison.Ordinal);
Assert.Contains("(uRenderPass & 0x200) == 0", shader, StringComparison.Ordinal);
Assert.DoesNotContain("alpha <= alphaCutoff", shader, StringComparison.Ordinal); Assert.DoesNotContain("alpha <= alphaCutoff", shader, StringComparison.Ordinal);
Assert.DoesNotContain("color.a <= alphaCutoff", shader, StringComparison.Ordinal); Assert.DoesNotContain("color.a <= alphaCutoff", shader, StringComparison.Ordinal);
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", shader, StringComparison.Ordinal); Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", shader, StringComparison.Ordinal);
@ -409,6 +517,38 @@ public sealed class EnvCellAlphaDrawSourceTests
return [.. constants]; return [.. constants];
} }
private static void AssertVector(Vector4 expected, Vector4 actual)
{
Assert.Equal(expected.X, actual.X, 6);
Assert.Equal(expected.Y, actual.Y, 6);
Assert.Equal(expected.Z, actual.Z, 6);
Assert.Equal(expected.W, actual.W, 6);
}
private static Vector4 IndependentComposite(
Vector4 source,
Vector4 destination,
RetailDetailTextureContract.FramebufferFamily family)
{
float x = source.W;
return family switch
{
RetailDetailTextureContract.FramebufferFamily.Opaque => source,
RetailDetailTextureContract.FramebufferFamily.Alpha =>
source * x + destination * (1f - x),
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive =>
source * x + destination,
RetailDetailTextureContract.FramebufferFamily.Additive => source + destination,
RetailDetailTextureContract.FramebufferFamily.InverseAlpha =>
source * (1f - x) + destination * x,
RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive =>
source * (1f - x) + destination,
RetailDetailTextureContract.FramebufferFamily.Clip =>
source + destination * new Vector4(1f - x),
_ => throw new ArgumentOutOfRangeException(nameof(family)),
};
}
private static string RepositoryRoot() private static string RepositoryRoot()
{ {
DirectoryInfo? cursor = new(AppContext.BaseDirectory); DirectoryInfo? cursor = new(AppContext.BaseDirectory);
@ -419,46 +559,32 @@ public sealed class EnvCellAlphaDrawSourceTests
} }
private readonly record struct BatchSpec( private readonly record struct BatchSpec(
byte Mask, SurfaceType Type,
bool IsTransparent, bool Paletted,
AcDream.Core.Meshing.TranslucencyKind Translucency, bool PositiveStippling)
uint PaletteId)
{ {
internal static BatchSpec Opaque { get; } = new( internal static BatchSpec Opaque { get; } = new(
0x00, SurfaceType.Base1Image, Paletted: false, PositiveStippling: false);
IsTransparent: false,
AcDream.Core.Meshing.TranslucencyKind.Opaque,
PaletteId: 0);
internal static BatchSpec ClipDds { get; } = new( internal static BatchSpec ClipDds { get; } = new(
RetailAlphaMeshRouter.MaskClipMap, SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
IsTransparent: true, Paletted: false, PositiveStippling: false);
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
PaletteId: 0);
internal static BatchSpec ClipPaletted { get; } = new( internal static BatchSpec ClipPaletted { get; } = new(
RetailAlphaMeshRouter.MaskClipMap, SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
IsTransparent: true, Paletted: true, PositiveStippling: false);
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
PaletteId: 0x04000001u);
internal static BatchSpec ClipPositiveStippleDds { get; } = new( internal static BatchSpec ClipPositiveStippleDds { get; } = new(
RetailAlphaMeshRouter.MaskClipMap | RetailAlphaMeshRouter.MaskPositiveStipple, SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
IsTransparent: true, Paletted: false, PositiveStippling: true);
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
PaletteId: 0);
internal static BatchSpec Alpha { get; } = new( internal static BatchSpec Alpha { get; } = new(
RetailAlphaMeshRouter.MaskAlphaFamily, SurfaceType.Base1Image | SurfaceType.Alpha,
IsTransparent: true, Paletted: false, PositiveStippling: false);
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
PaletteId: 0);
internal static BatchSpec Additive { get; } = new( internal static BatchSpec Additive { get; } = new(
RetailAlphaMeshRouter.MaskAlphaFamily, SurfaceType.Base1Image | SurfaceType.Additive,
IsTransparent: true, Paletted: false, PositiveStippling: false);
AcDream.Core.Meshing.TranslucencyKind.Additive,
PaletteId: 0);
} }
/// <summary> /// <summary>
@ -512,27 +638,45 @@ public sealed class EnvCellAlphaDrawSourceTests
for (int i = 0; i < specs.Length; i++) for (int i = 0; i < specs.Length; i++)
{ {
BatchSpec spec = specs[i]; BatchSpec spec = specs[i];
TranslucencyKind translucency =
TranslucencyKindExtensions.FromSurfaceType(spec.Type);
bool alphaFamily = (spec.Type
& (SurfaceType.Alpha | SurfaceType.InvAlpha | SurfaceType.Additive)) != 0;
byte mask = RetailAlphaMeshRouter.ConstructSubsetMask(
alphaFamily,
(spec.Type & SurfaceType.Base1ClipMap) != 0,
(spec.Type & SurfaceType.Translucent) != 0,
spec.PositiveStippling);
uint paletteId = spec.Paletted ? 0x04000001u : 0u;
batches.Add(new TextureBatchData batches.Add(new TextureBatchData
{ {
Key = new TextureKey Key = new TextureKey
{ {
SurfaceId = 0x08000BFFu + (uint)i, SurfaceId = 0x08000BFFu + (uint)i,
PaletteId = spec.PaletteId, PaletteId = paletteId,
}, },
TextureData = new byte[8 * 8 * 4], TextureData = new byte[8 * 8 * 4],
Indices = [0, 1, 2], Indices = [0, 1, 2],
IsTransparent = spec.IsTransparent, IsTransparent = translucency != TranslucencyKind.Opaque,
IsAdditive = spec.Translucency == AcDream.Core.Meshing.TranslucencyKind.Additive, IsAdditive = (spec.Type & SurfaceType.Additive) != 0,
Translucency = spec.Translucency, Translucency = translucency,
MaterialState = RetailSetSurfaceMaterialState.Resolve(
spec.Type,
texturePresent: true,
textureHasPalette: spec.Paletted),
SurfaceOpacity = 0.25f, SurfaceOpacity = 0.25f,
RetailSurfaceMask = spec.Mask, RetailSurfaceMask = mask,
CullMode = CullMode.Clockwise, CullMode = CullMode.Clockwise,
IsCellShell = true, IsCellShell = true,
SourceSurfaceIndex = i, SourceSurfaceIndex = i,
}); });
} }
mesh.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches; mesh.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches;
Assert.NotNull(_meshManager.UploadMeshData(mesh)); ObjectRenderData uploaded = Assert.IsType<ObjectRenderData>(
_meshManager.UploadMeshData(mesh));
Assert.Equal(
batches.Select(static batch => batch.MaterialState),
uploaded.Batches.Select(static batch => batch.MaterialState));
Renderer = new EnvCellRenderer( Renderer = new EnvCellRenderer(
Device, Device,

View file

@ -80,12 +80,14 @@ public sealed class GpuContractTests
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while // pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
// detail is now computed inside those same pipelines. Collapsing any of // detail is now computed inside those same pipelines. Collapsing any of
// these modes changes the corresponding subset's composition. // these modes changes the corresponding subset's composition.
Assert.Equal(5, Enum.GetValues<GpuBlendMode>().Length); Assert.Equal(7, Enum.GetValues<GpuBlendMode>().Length);
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>()); Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>()); Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>()); Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>()); Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.RawAdditive, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>()); Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.InverseAdditive, Enum.GetValues<GpuBlendMode>());
} }
[Fact] [Fact]

View file

@ -45,7 +45,9 @@ public sealed class VulkanShaderManifestTests
// single-pass building detail material now runs inside the base // single-pass building detail material now runs inside the base
// fragment. Final alpha is authoredOpacity*liveFade*detail.a^2; // fragment. Final alpha is authoredOpacity*liveFade*detail.a^2;
// CLIP tests that value while detail-off retains base-alpha logic. // CLIP tests that value while detail-off retains base-alpha logic.
["mesh_modern.frag.spv"] = "576af229b96a2a5e9b3b1ff16ab686d96578c2d9d1762b0e225df31bfd7aa86f", // Fix round 1 additionally carries the exact SetSurface alpha-test
// reference and raw-additive fog suppression into this same pass.
["mesh_modern.frag.spv"] = "bd47fe8a33e0f1d025fe48fd95b034ba6fe591a6d20e88e636c86238d8773db1",
// S5-c4 replaces the padding word at batch offset 4 with authored // S5-c4 replaces the padding word at batch offset 4 with authored
// surface opacity and forwards it plus exact detail category/flags // surface opacity and forwards it plus exact detail category/flags
// flat to the fragment shader. The 16-byte ABI is unchanged. // flat to the fragment shader. The 16-byte ABI is unchanged.

View file

@ -143,6 +143,12 @@ public sealed class VulkanViewportMappingTests
Assert.Equal( Assert.Equal(
(BlendFactor.SrcAlpha, BlendFactor.One), (BlendFactor.SrcAlpha, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive)); VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive));
Assert.Equal(
(BlendFactor.One, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RawAdditive));
Assert.Equal(
(BlendFactor.OneMinusSrcAlpha, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAdditive));
// Retail's third mode, found at slice V4c. Mapping it onto straight // Retail's third mode, found at slice V4c. Mapping it onto straight
// alpha would have silently changed how every inverse-alpha surface // alpha would have silently changed how every inverse-alpha surface
// composites. // composites.

View file

@ -14,6 +14,45 @@ public sealed class RetailDetailTextureContractTests
private static readonly Vector4 Detail = new(0.91f, 0.23f, 0.49f, 0.62f); private static readonly Vector4 Detail = new(0.91f, 0.23f, 0.49f, 0.62f);
private static readonly Vector4 Destination = new(0.17f, 0.37f, 0.71f, 0.29f); private static readonly Vector4 Destination = new(0.17f, 0.37f, 0.71f, 0.29f);
public static TheoryData<SurfaceType, bool, RetailSetSurfaceBlend,
RetailSetSurfaceAlphaTest, bool> ResolvedRows() => new()
{
{ SurfaceType.Base1Image, false, RetailSetSurfaceBlend.Opaque, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Alpha, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Alpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.AlphaAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.InvAlpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Dds, true },
{ SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Paletted, true },
{ SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Paletted, true },
{ SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Dds, false },
{ SurfaceType.Translucent, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Translucent | SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, false },
};
[Theory]
[MemberData(nameof(ResolvedRows))]
public void SetSurfaceResolver_PreservesEveryRawFamilyClipAndLateOverride(
SurfaceType type,
bool paletted,
RetailSetSurfaceBlend expectedBlend,
RetailSetSurfaceAlphaTest expectedAlphaTest,
bool expectedFog)
{
RetailSetSurfaceMaterialState state = RetailSetSurfaceMaterialState.Resolve(
type,
texturePresent: true,
textureHasPalette: paletted);
Assert.Equal(expectedBlend, state.Blend);
Assert.Equal(expectedAlphaTest, state.AlphaTest);
Assert.Equal(expectedFog, state.FogEnabled);
Assert.Equal(state, RetailSetSurfaceMaterialState.FromPackedByte(state.ToPackedByte()));
}
public static TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float> PolicyRows() public static TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float> PolicyRows()
{ {
var rows = new TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float>(); var rows = new TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float>();
@ -65,27 +104,22 @@ public sealed class RetailDetailTextureContractTests
Assert.Equal(["source-subset"], PhysicalDrawSequence(detailEnabled)); Assert.Equal(["source-subset"], PhysicalDrawSequence(detailEnabled));
PipelineState state = ExpectedPipelineState( PipelineState state = ExpectedPipelineState(
consumer, expectedKind, paletted, expectedReference); consumer, surfaceType, expectedKind, paletted, detailEnabled);
Assert.Equal(expectedReference, state.AlphaReference); RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
Assert.Equal( surfaceType,
expectedKind == TranslucencyKind.ClipMap texturePresent: true,
? paletted ? 100f / 255f : 200f / 255f textureHasPalette: paletted);
: 0.05f, if (detailEnabled)
state.AlphaReference);
Assert.True(state.AlphaTest);
Assert.Equal(expectedKind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
state.DepthWrite);
Assert.Equal(expectedKind switch
{ {
TranslucencyKind.Opaque => GpuBlendMode.None, Assert.Equal(resolved.AlphaTestReference, state.AlphaReference);
TranslucencyKind.AlphaBlend => GpuBlendMode.StraightAlpha, Assert.Equal(resolved.AlphaTestEnabled, state.AlphaTest);
TranslucencyKind.Additive => GpuBlendMode.Additive, Assert.Equal(resolved.FogEnabled, state.FogEnabled);
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha, }
TranslucencyKind.ClipMap when consumer == "EnvCell" => else
GpuBlendMode.PremultipliedAlpha, {
TranslucencyKind.ClipMap => GpuBlendMode.None, Assert.Equal(expectedReference, state.AlphaReference);
_ => throw new ArgumentOutOfRangeException(nameof(expectedKind)), Assert.True(state.AlphaTest);
}, state.Blend); }
Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled)); Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled));
if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap) if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap)
Assert.Equal("AP-240 immediate A2C", state.Placement); Assert.Equal("AP-240 immediate A2C", state.Placement);
@ -263,11 +297,38 @@ public sealed class RetailDetailTextureContractTests
private static PipelineState ExpectedPipelineState( private static PipelineState ExpectedPipelineState(
string consumer, string consumer,
SurfaceType surfaceType,
TranslucencyKind kind, TranslucencyKind kind,
bool paletted, bool paletted,
float reference) bool detailEnabled)
{ {
_ = paletted; RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
surfaceType,
texturePresent: true,
textureHasPalette: paletted);
if (detailEnabled)
{
GpuBlendMode exactBlend = resolved.Blend switch
{
RetailSetSurfaceBlend.Opaque => GpuBlendMode.None,
RetailSetSurfaceBlend.StraightAlpha => GpuBlendMode.StraightAlpha,
RetailSetSurfaceBlend.AlphaAdditive => GpuBlendMode.Additive,
RetailSetSurfaceBlend.Additive => GpuBlendMode.RawAdditive,
RetailSetSurfaceBlend.InverseAlpha => GpuBlendMode.InverseAlpha,
RetailSetSurfaceBlend.InverseAdditive => GpuBlendMode.InverseAdditive,
RetailSetSurfaceBlend.Clip => GpuBlendMode.PremultipliedAlpha,
_ => throw new ArgumentOutOfRangeException(),
};
return new PipelineState(
exactBlend,
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
resolved.AlphaTestEnabled,
resolved.AlphaTestReference,
resolved.FogEnabled,
consumer == "Building" && kind == TranslucencyKind.ClipMap
? "AP-240 immediate A2C"
: "source FIFO position");
}
GpuBlendMode blend = kind switch GpuBlendMode blend = kind switch
{ {
TranslucencyKind.Opaque => GpuBlendMode.None, TranslucencyKind.Opaque => GpuBlendMode.None,
@ -283,7 +344,10 @@ public sealed class RetailDetailTextureContractTests
blend, blend,
kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap, kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
AlphaTest: true, AlphaTest: true,
reference, kind == TranslucencyKind.ClipMap
? paletted ? 100f / 255f : 200f / 255f
: 0.05f,
FogEnabled: true,
consumer == "Building" && kind == TranslucencyKind.ClipMap consumer == "Building" && kind == TranslucencyKind.ClipMap
? "AP-240 immediate A2C" ? "AP-240 immediate A2C"
: "source FIFO position"); : "source FIFO position");
@ -310,6 +374,7 @@ public sealed class RetailDetailTextureContractTests
bool DepthWrite, bool DepthWrite,
bool AlphaTest, bool AlphaTest,
float AlphaReference, float AlphaReference,
bool FogEnabled,
string Placement); string Placement);
private static Vector4 IndependentComposite( private static Vector4 IndependentComposite(

View file

@ -32,7 +32,8 @@ public sealed class OrderPreservingSubmitterTests
WalkDrawStage stage = WalkDrawStage.Terrain, WalkDrawStage stage = WalkDrawStage.Terrain,
TranslucencyKind translucency = TranslucencyKind.Opaque, TranslucencyKind translucency = TranslucencyKind.Opaque,
CullMode cullMode = CullMode.CounterClockwise, CullMode cullMode = CullMode.CounterClockwise,
uint detailCategory = 0) => uint detailCategory = 0,
RetailSetSurfaceMaterialState? materialState = null) =>
new( new(
Key: new GroupKey( Key: new GroupKey(
FirstIndex: (uint)index * 3, FirstIndex: (uint)index * 3,
@ -41,6 +42,7 @@ public sealed class OrderPreservingSubmitterTests
TextureSlot: new GpuTextureSlot((uint)index), TextureSlot: new GpuTextureSlot((uint)index),
TextureLayer: 0, TextureLayer: 0,
Translucency: translucency, Translucency: translucency,
MaterialState: materialState ?? RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0, FoliageFlags: 0,
CullMode: cullMode), CullMode: cullMode),
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3), Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),
@ -284,8 +286,12 @@ public sealed class OrderPreservingSubmitterTests
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device)); Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
} }
[Fact] [Theory]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace() [InlineData(false, 200f / 255f)]
[InlineData(true, 100f / 255f)]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace(
bool paletted,
float expectedReference)
{ {
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true); using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
using DrawScope draw = fx.BeginDraw(); using DrawScope draw = fx.BeginDraw();
@ -293,7 +299,14 @@ public sealed class OrderPreservingSubmitterTests
OrderedDrawStream stream = StreamOf( OrderedDrawStream stream = StreamOf(
MakeCommand(0), MakeCommand(0),
MakeCommand(1, detailCategory: 1), MakeCommand(1, detailCategory: 1),
MakeCommand(2, translucency: TranslucencyKind.ClipMap, detailCategory: 1), MakeCommand(
2,
translucency: TranslucencyKind.ClipMap,
detailCategory: 1,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Base1ClipMap,
texturePresent: true,
textureHasPalette: paletted)),
MakeCommand(3)); MakeCommand(3));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream); PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
@ -317,8 +330,90 @@ public sealed class OrderPreservingSubmitterTests
.Select(call => call.PipelineName) .Select(call => call.PipelineName)
.ToArray()); .ToArray());
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count()); Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
Assert.Equal(expectedReference, runs[2].Constants.ParamB);
GpuPipelineDescription a2c = fx.Device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == "wb-mesh-opaque-a2c-1x")
.Description;
Assert.Equal(GpuBlendMode.None, a2c.Blend);
Assert.True(a2c.AlphaToCoverage);
Assert.True(a2c.Depth.Test);
Assert.True(a2c.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, a2c.Depth.Compare);
} }
[Fact]
public void PrepareThenDraw_PureClipBuildingDetailOffRetainsAp240A2cAndNeutralDetailState()
{
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: false);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(
1,
translucency: TranslucencyKind.ClipMap,
detailCategory: 1,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Base1ClipMap,
texturePresent: true,
textureHasPalette: true)),
MakeCommand(2));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
Assert.Equal([(0, 1), (1, 1), (2, 1)],
runs.Select(run => (run.Start, run.Count)).ToList());
Assert.Equal(0u, runs[1].Constants.TextureIndexA);
Assert.Equal(0f, runs[1].Constants.ParamA);
Assert.Equal(0f, runs[1].Constants.ParamB);
Assert.Equal(0, runs[1].Constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == "wb-mesh-opaque-a2c-1x");
}
[Fact]
public void AtmosphericReceiver_AdjacentOrdinaryDetailOrdinaryCommandsDoNotLeakExactState()
{
using var fx = new DispatcherFixture(
detailAvailable: true,
detailEnabled: true,
atmospheric: true);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(
1,
translucency: TranslucencyKind.Additive,
detailCategory: 1,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Additive,
texturePresent: true,
textureHasPalette: false)),
MakeCommand(2));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
Assert.Equal([(0, 1), (1, 1), (2, 1)],
runs.Select(run => (run.Start, run.Count)).ToList());
Assert.Equal((0u, 0f, 0f, 0), DetailState(runs[0].Constants));
Assert.Equal((77u, 3.5f, 0f, RetailDetailTextureContract.NoFogRenderPassFlag),
DetailState(runs[1].Constants));
Assert.Equal((0u, 0f, 0f, 0), DetailState(runs[2].Constants));
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == "wb-mesh-atmospheric-raw-additive-1x");
GpuPipelineDescription selected = fx.Device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == "wb-mesh-atmospheric-raw-additive-1x")
.Description;
Assert.Equal("mesh_atmospheric", selected.Shaders.Name);
Assert.Equal(GpuBlendMode.RawAdditive, selected.Blend);
}
private static (uint Slot, float Tiling, float Reference, int NoFog) DetailState(
GpuPushConstants constants) =>
(constants.TextureIndexA, constants.ParamA, constants.ParamB,
constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
[Fact] [Fact]
public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne() public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne()
{ {
@ -609,7 +704,10 @@ public sealed class OrderPreservingSubmitterTests
private readonly WbMeshAdapter _meshAdapter; private readonly WbMeshAdapter _meshAdapter;
private readonly TextureCache _textures; private readonly TextureCache _textures;
public DispatcherFixture(bool detailAvailable = false, bool detailEnabled = false) public DispatcherFixture(
bool detailAvailable = false,
bool detailEnabled = false,
bool atmospheric = false)
{ {
Device = new RecordingGpuDevice(); Device = new RecordingGpuDevice();
FrameLifetime = new GpuDeviceFrameLifetime(Device); FrameLifetime = new GpuDeviceFrameLifetime(Device);
@ -640,6 +738,15 @@ public sealed class OrderPreservingSubmitterTests
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16) new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
: default, : default,
buildingDetailEnabled: () => detailEnabled); buildingDetailEnabled: () => detailEnabled);
if (atmospheric)
{
var source = new BindableAtmosphericSource();
WbDrawDispatcher.DirectionalShadowReceiverPipelineState candidate =
Assert.IsType<WbDrawDispatcher.DirectionalShadowReceiverPipelineState>(
Dispatcher.PrepareDirectionalShadowReceiver(source, sampleCount: 1));
Assert.Null(Dispatcher.SwapDirectionalShadowReceiver(candidate));
}
Atmospheric = atmospheric;
} }
public RecordingGpuDevice Device { get; } public RecordingGpuDevice Device { get; }
@ -650,6 +757,8 @@ public sealed class OrderPreservingSubmitterTests
public WbDrawDispatcher Dispatcher { get; } public WbDrawDispatcher Dispatcher { get; }
public bool Atmospheric { get; }
/// <summary>Opens a frame and a backbuffer pass, publishes it on /// <summary>Opens a frame and a backbuffer pass, publishes it on
/// <see cref="Scope"/>, then clears the recorded calls so a test only /// <see cref="Scope"/>, then clears the recorded calls so a test only
/// sees what its own <c>SubmitOrderedStream</c> call produced.</summary> /// sees what its own <c>SubmitOrderedStream</c> call produced.</summary>
@ -659,7 +768,11 @@ public sealed class OrderPreservingSubmitterTests
IGpuFrame frame = FrameLifetime.CurrentFrame!; IGpuFrame frame = FrameLifetime.CurrentFrame!;
IGpuPassEncoder pass = frame.BeginPass( IGpuPassEncoder pass = frame.BeginPass(
GpuPassDescription.BackbufferClear( GpuPassDescription.BackbufferClear(
"fw2-ordered-stream-test", Vector4.Zero, sampleCount: 1)); Atmospheric
? DirectionalShadowReceiverPolicy.AtmosphericWorldPassName
: "fw2-ordered-stream-test",
Vector4.Zero,
sampleCount: 1));
IDisposable publication = Scope.Publish(pass); IDisposable publication = Scope.Publish(pass);
Device.Clear(); Device.Clear();
return new DrawScope(frame, pass, publication); return new DrawScope(frame, pass, publication);
@ -674,6 +787,30 @@ public sealed class OrderPreservingSubmitterTests
} }
} }
private sealed class BindableAtmosphericSource : IDirectionalShadowReceiverSource
{
public DirectionalShadowPipelineShaders PipelineShaders =>
DirectionalShadowPipelineShaders.Local;
public bool TryGetCurrentFrameBinding(
IGpuFrame frame,
out DirectionalShadowFrameBinding binding)
{
GpuRingAllocation allocation = frame.AllocateRing(
checked((int)DirectionalShadowUniforms.SizeInBytes),
GpuRingUsage.Uniform);
binding = new DirectionalShadowFrameBinding(
frame.Serial,
Enabled: false,
allocation.Buffer,
allocation.OffsetBytes,
DirectionalShadowUniforms.SizeInBytes,
GpuTextureSlot.Unassigned,
CascadeCount: 0);
return true;
}
}
private sealed class NullPreparedAssetSource : IPreparedAssetSource private sealed class NullPreparedAssetSource : IPreparedAssetSource
{ {
public PreparedAssetSourceStats Stats => default; public PreparedAssetSourceStats Stats => default;

View file

@ -28,6 +28,7 @@ public sealed class OrderedDrawStreamTests
TextureSlot: new GpuTextureSlot((uint)index), TextureSlot: new GpuTextureSlot((uint)index),
TextureLayer: 0, TextureLayer: 0,
Translucency: translucency, Translucency: translucency,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0, FoliageFlags: 0,
CullMode: cullMode), CullMode: cullMode),
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3), Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),

View file

@ -105,11 +105,13 @@ public sealed class WalkStaticStreamPopulatorTests
int indexCount, int indexCount,
uint textureSlotIndex, uint textureSlotIndex,
uint textureLayer = 0, uint textureLayer = 0,
CullMode cullMode = CullMode.CounterClockwise) => CullMode cullMode = CullMode.CounterClockwise,
RetailSetSurfaceMaterialState? materialState = null) =>
new() new()
{ {
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false }, Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
Translucency = translucency, Translucency = translucency,
MaterialState = materialState ?? RetailSetSurfaceMaterialState.Opaque,
FirstIndex = firstIndex, FirstIndex = firstIndex,
BaseVertex = (uint)baseVertex, BaseVertex = (uint)baseVertex,
IndexCount = indexCount, IndexCount = indexCount,
@ -316,14 +318,14 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.DoesNotContain( Assert.DoesNotContain(
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase); "one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("160 active rows", register, StringComparison.Ordinal); Assert.Contains("active total to **161**", register, StringComparison.Ordinal);
int apSectionStart = register.IndexOf( int apSectionStart = register.IndexOf(
"## 3. Documented approximation (AP)", StringComparison.Ordinal); "## 3. Documented approximation (AP)", StringComparison.Ordinal);
int apSectionEnd = register.IndexOf( int apSectionEnd = register.IndexOf(
"## 4. Temporary stopgap (TS)", StringComparison.Ordinal); "## 4. Temporary stopgap (TS)", StringComparison.Ordinal);
Assert.True(apSectionStart >= 0, "AP section heading must exist."); Assert.True(apSectionStart >= 0, "AP section heading must exist.");
Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section."); Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section.");
foreach (string id in new[] { "AP-241", "AP-242", "AP-243" }) foreach (string id in new[] { "AP-241", "AP-242", "AP-243", "AP-244" })
{ {
int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal); int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal);
Assert.True( Assert.True(
@ -337,8 +339,10 @@ public sealed class WalkStaticStreamPopulatorTests
line.StartsWith("| AP-242 |", StringComparison.Ordinal)); line.StartsWith("| AP-242 |", StringComparison.Ordinal));
Assert.Single(File.ReadLines(registerPath), static line => Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-243 |", StringComparison.Ordinal)); line.StartsWith("| AP-243 |", StringComparison.Ordinal));
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-244 |", StringComparison.Ordinal));
Assert.Equal( Assert.Equal(
160, 161,
File.ReadLines(registerPath).Count(static line => File.ReadLines(registerPath).Count(static line =>
line.StartsWith("| AP-", StringComparison.Ordinal))); line.StartsWith("| AP-", StringComparison.Ordinal)));
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal); Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
@ -417,7 +421,12 @@ public sealed class WalkStaticStreamPopulatorTests
InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh( InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh(
MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1))); MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1)));
InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh( InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh(
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4, indexCount: 6, textureSlotIndex: 2))); MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4,
indexCount: 6, textureSlotIndex: 2,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Alpha | SurfaceType.Additive,
texturePresent: true,
textureHasPalette: false))));
var meshRefs = new[] var meshRefs = new[]
{ {
@ -448,6 +457,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Equal(3u, translucent.Key.FirstIndex); Assert.Equal(3u, translucent.Key.FirstIndex);
Assert.Equal(6, translucent.Key.IndexCount); Assert.Equal(6, translucent.Key.IndexCount);
Assert.Equal(2u, translucent.Key.TextureSlot.Index); Assert.Equal(2u, translucent.Key.TextureSlot.Index);
Assert.Equal(RetailSetSurfaceBlend.AlphaAdditive, translucent.Key.MaterialState.Blend);
Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform); Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform);
Assert.Equal(2, selectionParts.Count); Assert.Equal(2, selectionParts.Count);
@ -1095,7 +1105,8 @@ public sealed class WalkStaticStreamPopulatorTests
using var fx = new DispatcherFixture(withAlphaQueue: true); using var fx = new DispatcherFixture(withAlphaQueue: true);
fx.AlphaQueue!.BeginFrame(); fx.AlphaQueue!.BeginFrame();
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0); var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0);
Vector3 localSortCenter = new(1, 2, 3); Vector3 localSortCenter = new(1, 2, 3);
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6); Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
var batch = new WbDrawDispatcher.WalkClassifiedBatch( var batch = new WbDrawDispatcher.WalkClassifiedBatch(
@ -1221,13 +1232,35 @@ public sealed class WalkStaticStreamPopulatorTests
} }
[Theory] [Theory]
[InlineData(TranslucencyKind.AlphaBlend, "wb-mesh-alpha-1x")] [InlineData(SurfaceType.Alpha, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)]
[InlineData(TranslucencyKind.Additive, "wb-mesh-additive-1x")] [InlineData(SurfaceType.Alpha | SurfaceType.Additive, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 0f, false)]
[InlineData(TranslucencyKind.InvAlpha, "wb-mesh-inverse-1x")] [InlineData(SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)]
public void ImmediateBuildingDetail_RetainsOriginalFramebufferFamily( [InlineData(SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)]
TranslucencyKind kind, [InlineData(SurfaceType.InvAlpha | SurfaceType.Additive, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 0f, false)]
string expectedPipeline) [InlineData(SurfaceType.Translucent, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)]
[InlineData(SurfaceType.Translucent | SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 200f / 255f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 100f / 255f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 200f / 255f, false)]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-additive-1x", GpuBlendMode.Additive, 100f / 255f, false)]
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 200f / 255f, false)]
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 100f / 255f, false)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 200f / 255f, true)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, true, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 100f / 255f, true)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 200f / 255f, false)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 100f / 255f, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive,
true, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, false)]
public void ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState(
SurfaceType type,
bool paletted,
string expectedPipeline,
object expectedBlendValue,
float expectedReference,
bool expectedFog)
{ {
var expectedBlend = (GpuBlendMode)expectedBlendValue;
using var fx = new DispatcherFixture( using var fx = new DispatcherFixture(
withAlphaQueue: true, withAlphaQueue: true,
detailAvailable: true, detailAvailable: true,
@ -1235,7 +1268,8 @@ public sealed class WalkStaticStreamPopulatorTests
using DrawScope draw = fx.BeginDraw(beginAlpha: true); using DrawScope draw = fx.BeginDraw(beginAlpha: true);
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch( WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
detailCategory: 1u, detailCategory: 1u,
translucency: kind); surfaceType: type,
paletted: paletted);
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity); fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
@ -1243,11 +1277,121 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>()); Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(), Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == expectedPipeline); call => call.PipelineName == expectedPipeline);
GpuPushConstants armed = fx.Device.Calls
.OfType<GpuRecordedPushConstants>()
.Last(call => call.Constants.ParamA != 0f)
.Constants;
Assert.Equal(99u, armed.TextureIndexA);
Assert.Equal(2f, armed.ParamA);
Assert.Equal(expectedReference, armed.ParamB);
Assert.Equal(!expectedFog,
(armed.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag) != 0);
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
type, texturePresent: true, textureHasPalette: paletted);
GpuPipelineDescription selected = fx.Device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == expectedPipeline)
.Description;
Assert.Equal(expectedBlend, selected.Blend);
Assert.True(selected.Depth.Test);
Assert.Equal(
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
selected.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, selected.Depth.Compare);
Vector4 source = RetailDetailTextureContract.Combine(
new Vector4(0.31f, 0.57f, 0.83f, 0.19f),
new Vector3(0.73f, 0.41f, 0.67f),
new Vector4(0.91f, 0.23f, 0.49f, 0.62f),
authoredOpacity: 0.75f,
liveOpacity: 0.4f);
Assert.Equal(0.3534682f, source.X, 6);
Assert.Equal(0.2330118f, source.Y, 6);
Assert.Equal(0.5438054f, source.Z, 6);
Assert.Equal(0.11532f, source.W, 6);
Vector4 destination = new(0.17f, 0.37f, 0.71f, 0.29f);
float x = source.W;
RetailDetailTextureContract.FramebufferFamily family = expectedBlend switch
{
GpuBlendMode.StraightAlpha => RetailDetailTextureContract.FramebufferFamily.Alpha,
GpuBlendMode.Additive => RetailDetailTextureContract.FramebufferFamily.AlphaAdditive,
GpuBlendMode.RawAdditive => RetailDetailTextureContract.FramebufferFamily.Additive,
GpuBlendMode.InverseAlpha => RetailDetailTextureContract.FramebufferFamily.InverseAlpha,
GpuBlendMode.InverseAdditive => RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive,
_ => throw new ArgumentOutOfRangeException(nameof(expectedBlend)),
};
Vector4 expectedFramebuffer = family switch
{
RetailDetailTextureContract.FramebufferFamily.Alpha => source * x + destination * (1f - x),
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive => source * x + destination,
RetailDetailTextureContract.FramebufferFamily.Additive => source + destination,
RetailDetailTextureContract.FramebufferFamily.InverseAlpha => source * (1f - x) + destination * x,
RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive => source * (1f - x) + destination,
_ => throw new ArgumentOutOfRangeException(nameof(family)),
};
Vector4 framebuffer = RetailDetailTextureContract.Composite(source, destination, family);
Assert.Equal(expectedFramebuffer.X, framebuffer.X, 6);
Assert.Equal(expectedFramebuffer.Y, framebuffer.Y, 6);
Assert.Equal(expectedFramebuffer.Z, framebuffer.Z, 6);
Assert.Equal(expectedFramebuffer.W, framebuffer.W, 6);
if (resolved.AlphaTestEnabled)
{
Assert.False(RetailDetailTextureContract.SurvivesClip(
MathF.BitDecrement(expectedReference), expectedReference));
Assert.True(RetailDetailTextureContract.SurvivesClip(expectedReference, expectedReference));
Assert.True(RetailDetailTextureContract.SurvivesClip(
MathF.BitIncrement(expectedReference), expectedReference));
}
}
[Theory]
[InlineData(SurfaceType.Alpha, "wb-mesh-alpha-1x")]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Additive, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.InvAlpha, "wb-mesh-inverse-1x")]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, "wb-mesh-alpha-1x")]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, "wb-mesh-inverse-1x")]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Translucent, "wb-mesh-alpha-1x")]
[InlineData(SurfaceType.Translucent | SurfaceType.Additive, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha, "wb-mesh-inverse-1x")]
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, "wb-mesh-alpha-1x")]
public void BuildingDetailOff_EveryRawStateRetainsThePreFixLogicalPath(
SurfaceType type,
string expectedPipeline)
{
using var fx = new DispatcherFixture(
withAlphaQueue: true,
detailAvailable: true,
detailEnabled: false);
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
detailCategory: 1u,
surfaceType: type);
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
Assert.Equal(1, fx.AlphaQueue!.PendingCount);
Assert.Empty(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
fx.AlphaQueue.EndFrame();
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == expectedPipeline);
GpuPushConstants constants = fx.Device.Calls
.OfType<GpuRecordedPushConstants>()
.Last()
.Constants;
Assert.Equal(0u, constants.TextureIndexA);
Assert.Equal(0f, constants.ParamA);
Assert.Equal(0f, constants.ParamB);
Assert.Equal(0, constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
} }
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch( private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(
uint detailCategory, uint detailCategory,
TranslucencyKind translucency = TranslucencyKind.AlphaBlend) => SurfaceType surfaceType = SurfaceType.Alpha,
bool paletted = false) =>
new( new(
new GroupKey( new GroupKey(
0, 0,
@ -1255,7 +1399,11 @@ public sealed class WalkStaticStreamPopulatorTests
3, 3,
new GpuTextureSlot(1), new GpuTextureSlot(1),
0, 0,
translucency, TranslucencyKindExtensions.FromSurfaceType(surfaceType),
RetailSetSurfaceMaterialState.Resolve(
surfaceType,
texturePresent: true,
textureHasPalette: paletted),
FoliageFlags: 0), FoliageFlags: 0),
Matrix4x4.Identity, Matrix4x4.Identity,
ClipSlot: 0, ClipSlot: 0,
@ -1277,6 +1425,10 @@ public sealed class WalkStaticStreamPopulatorTests
TextureSlot = new GpuTextureSlot(1), TextureSlot = new GpuTextureSlot(1),
TextureLayer = 0, TextureLayer = 0,
Translucency = TranslucencyKind.AlphaBlend, Translucency = TranslucencyKind.AlphaBlend,
MaterialState = RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Alpha,
texturePresent: true,
textureHasPalette: false),
}; };
group.Matrices.Add(Matrix4x4.Identity); group.Matrices.Add(Matrix4x4.Identity);
group.SubmissionOrders.Add(0); group.SubmissionOrders.Add(0);
@ -1320,7 +1472,8 @@ public sealed class WalkStaticStreamPopulatorTests
using var fx = new DispatcherFixture(withAlphaQueue: true); using var fx = new DispatcherFixture(withAlphaQueue: true);
fx.AlphaQueue!.BeginFrame(); fx.AlphaQueue!.BeginFrame();
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0); var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0);
var batch = new WbDrawDispatcher.WalkClassifiedBatch( var batch = new WbDrawDispatcher.WalkClassifiedBatch(
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10)); Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
@ -1354,7 +1507,8 @@ public sealed class WalkStaticStreamPopulatorTests
var stream = new OrderedDrawStream(); var stream = new OrderedDrawStream();
stream.Append(new OrderedDrawCommand( stream.Append(new OrderedDrawCommand(
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise), new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0, Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0)); WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));

View file

@ -28,6 +28,7 @@ using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content; using AcDream.Content;
using AcDream.Core.Meshing;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Xunit; using Xunit;
using CullMode = DatReaderWriter.Enums.CullMode; using CullMode = DatReaderWriter.Enums.CullMode;
@ -186,7 +187,8 @@ public class EnvCellRendererTests
"_mdiDrawRanges", BindingFlags.NonPublic | BindingFlags.Instance)!; "_mdiDrawRanges", BindingFlags.NonPublic | BindingFlags.Instance)!;
var ranges = (List<EnvCellRenderer.MdiDrawRange>)rangesField.GetValue(renderer)!; var ranges = (List<EnvCellRenderer.MdiDrawRange>)rangesField.GetValue(renderer)!;
ranges.Clear(); ranges.Clear();
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 0, firstCommand: 0, commandCount: 1); EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 0, firstCommand: 0, commandCount: 1,
RetailSetSurfaceMaterialState.Opaque);
var allInstances = new List<InstanceData>(); var allInstances = new List<InstanceData>();
for (int i = 0; i < instanceCount; i++) for (int i = 0; i < instanceCount; i++)
@ -239,7 +241,7 @@ public class EnvCellRendererTests
} }
[Fact] [Fact]
public void DetailUsesOnlyTheOriginalFourShellPipelines() public void DetailUsesOnlySetSurfaceShellPipelinesAndNoReplayPipeline()
{ {
using var device = new RecordingGpuDevice(); using var device = new RecordingGpuDevice();
using var meshManager = CreateMeshManager(device); using var meshManager = CreateMeshManager(device);
@ -250,7 +252,7 @@ public class EnvCellRendererTests
meshManager, meshManager,
new WbFrustum()); new WbFrustum());
Assert.Equal(4, device.CreatedPipelines.Count); Assert.Equal(7, device.CreatedPipelines.Count);
Assert.DoesNotContain(device.CreatedPipelines, Assert.DoesNotContain(device.CreatedPipelines,
pipeline => pipeline.Description.Name.Contains("detail", StringComparison.Ordinal)); pipeline => pipeline.Description.Name.Contains("detail", StringComparison.Ordinal));
} }
@ -260,11 +262,14 @@ public class EnvCellRendererTests
{ {
var ranges = new List<EnvCellRenderer.MdiDrawRange>(); var ranges = new List<EnvCellRenderer.MdiDrawRange>();
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3); EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3,
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 3, commandCount: 4); RetailSetSurfaceMaterialState.Opaque);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 3, commandCount: 4,
RetailSetSurfaceMaterialState.Opaque);
Assert.Equal( Assert.Equal(
[new EnvCellRenderer.MdiDrawRange(GroupIndex: 2, FirstCommand: 0, CommandCount: 7)], [new EnvCellRenderer.MdiDrawRange(GroupIndex: 2, FirstCommand: 0, CommandCount: 7,
RetailSetSurfaceMaterialState.Opaque)],
ranges); ranges);
} }
@ -273,17 +278,21 @@ public class EnvCellRendererTests
{ {
var ranges = new List<EnvCellRenderer.MdiDrawRange>(); var ranges = new List<EnvCellRenderer.MdiDrawRange>();
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3); EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3,
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 6, firstCommand: 3, commandCount: 2); RetailSetSurfaceMaterialState.Opaque);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 5, commandCount: 1); EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 6, firstCommand: 3, commandCount: 2,
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 9, commandCount: 2); RetailSetSurfaceMaterialState.Opaque);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 5, commandCount: 1,
RetailSetSurfaceMaterialState.Opaque);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 9, commandCount: 2,
RetailSetSurfaceMaterialState.Opaque);
Assert.Equal( Assert.Equal(
[ [
new EnvCellRenderer.MdiDrawRange(2, 0, 3), new EnvCellRenderer.MdiDrawRange(2, 0, 3, RetailSetSurfaceMaterialState.Opaque),
new EnvCellRenderer.MdiDrawRange(6, 3, 2), new EnvCellRenderer.MdiDrawRange(6, 3, 2, RetailSetSurfaceMaterialState.Opaque),
new EnvCellRenderer.MdiDrawRange(2, 5, 1), new EnvCellRenderer.MdiDrawRange(2, 5, 1, RetailSetSurfaceMaterialState.Opaque),
new EnvCellRenderer.MdiDrawRange(2, 9, 2), new EnvCellRenderer.MdiDrawRange(2, 9, 2, RetailSetSurfaceMaterialState.Opaque),
], ],
ranges); ranges);
} }
@ -293,7 +302,8 @@ public class EnvCellRendererTests
{ {
var ranges = new List<EnvCellRenderer.MdiDrawRange>(); var ranges = new List<EnvCellRenderer.MdiDrawRange>();
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 0); EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 0,
RetailSetSurfaceMaterialState.Opaque);
Assert.Empty(ranges); Assert.Empty(ranges);
} }

View file

@ -162,6 +162,7 @@ public sealed class FoliageWindClassificationTests
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(42), TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(42),
TextureLayer: 0, TextureLayer: 0,
Translucency: TranslucencyKind.ClipMap, Translucency: TranslucencyKind.ClipMap,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise, CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise,
FoliageFlags: sceneryFlags); FoliageFlags: sceneryFlags);
var landblockStaticKey = sceneryKey with { FoliageFlags = landblockStaticFlags }; var landblockStaticKey = sceneryKey with { FoliageFlags = landblockStaticFlags };

View file

@ -270,6 +270,7 @@ public class InstanceGroupClearTests
TextureSlot: Slot(0xAA), TextureSlot: Slot(0xAA),
TextureLayer: 0, TextureLayer: 0,
Translucency: TranslucencyKind.Opaque, Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0u); FoliageFlags: 0u);
var cached = new CachedBatch( var cached = new CachedBatch(
key, key,
@ -312,6 +313,7 @@ public class InstanceGroupClearTests
TextureSlot: Slot(0x77), TextureSlot: Slot(0x77),
TextureLayer: 0, TextureLayer: 0,
Translucency: TranslucencyKind.ClipMap, Translucency: TranslucencyKind.ClipMap,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0x2u, FoliageFlags: 0x2u,
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise); CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise);
@ -416,6 +418,7 @@ public class InstanceGroupClearTests
TextureSlot: Slot(textureSlot), TextureSlot: Slot(textureSlot),
TextureLayer: 0, TextureLayer: 0,
Translucency: TranslucencyKind.Opaque, Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0u); FoliageFlags: 0u);
private static WbDrawDispatcher.InstanceGroup MakeGroup( private static WbDrawDispatcher.InstanceGroup MakeGroup(

View file

@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using AcDream.Content; using AcDream.Content;
using AcDream.Core.Meshing;
using Chorizite.Core.Render.Enums; using Chorizite.Core.Render.Enums;
using DatReaderWriter; using DatReaderWriter;
using DatReaderWriter.DBObjs; using DatReaderWriter.DBObjs;
@ -255,6 +256,60 @@ public sealed class MeshExtractorSolidFaceExtractionTests
BitConverter.SingleToInt32Bits(batch.SurfaceOpacity)); BitConverter.SingleToInt32Bits(batch.SurfaceOpacity));
} }
public static TheoryData<SurfaceType, bool, RetailSetSurfaceBlend,
RetailSetSurfaceAlphaTest, bool> SetSurfaceExtractionRows() => new()
{
{ SurfaceType.Base1Image, false, RetailSetSurfaceBlend.Opaque, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.AlphaAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Base1Image | SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Dds, true },
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Paletted, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Paletted, true },
{ SurfaceType.Base1Image | SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, false },
};
[Theory]
[MemberData(nameof(SetSurfaceExtractionRows))]
public void OrdinaryAndCellExtraction_CarryIdenticalResolvedSetSurfaceState(
SurfaceType type,
bool paletted,
RetailSetSurfaceBlend expectedBlend,
RetailSetSurfaceAlphaTest expectedAlphaTest,
bool expectedFog)
{
var dats = new FakeMeshExtractorDats();
uint paletteId = paletted ? 0x04000001u : 0u;
RegisterTexturedQuad(
dats,
type,
PixelFormat.PFID_A8R8G8B8,
new byte[4 * 4 * 4],
paletteId: paletteId);
var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null);
ObjectMeshData ordinary = Assert.IsType<ObjectMeshData>(
extractor.PrepareMeshData(GfxObjId, isSetup: false));
ObjectMeshData cell = Assert.IsType<ObjectMeshData>(
extractor.PrepareCellStructMeshData(
id: 1,
BuildQuadCellStruct(RetailCullMode.Landblock),
surfaceOverrides: [2],
Matrix4x4.Identity,
CancellationToken.None));
RetailSetSurfaceMaterialState ordinaryState =
Assert.Single(Assert.Single(ordinary.TextureBatches).Value).MaterialState;
RetailSetSurfaceMaterialState cellState =
Assert.Single(Assert.Single(cell.TextureBatches).Value).MaterialState;
Assert.Equal(ordinaryState, cellState);
Assert.Equal(expectedBlend, ordinaryState.Blend);
Assert.Equal(expectedAlphaTest, ordinaryState.AlphaTest);
Assert.Equal(expectedFog, ordinaryState.FogEnabled);
}
/// <summary> /// <summary>
/// OH2/S1 chunk-2 (2026-09-02): these two winding tests used to register /// OH2/S1 chunk-2 (2026-09-02): these two winding tests used to register
/// an UNTEXTURED (Base1Solid) surface. Under the exact contract, an /// an UNTEXTURED (Base1Solid) surface. Under the exact contract, an
@ -800,7 +855,8 @@ public sealed class MeshExtractorSolidFaceExtractionTests
SurfaceType surfaceType, SurfaceType surfaceType,
PixelFormat pixelFormat, PixelFormat pixelFormat,
byte[] sourceData, byte[] sourceData,
float translucency = 0.0f) float translucency = 0.0f,
uint paletteId = 0)
{ {
dats.RegisterRootGfxObj(GfxObjId, BuildQuadGfxObj(TexturedSurfaceId, noPos: false)); dats.RegisterRootGfxObj(GfxObjId, BuildQuadGfxObj(TexturedSurfaceId, noPos: false));
dats.Register(TexturedSurfaceId, new Surface dats.Register(TexturedSurfaceId, new Surface
@ -818,6 +874,7 @@ public sealed class MeshExtractorSolidFaceExtractionTests
Width = 4, Width = 4,
Height = 4, Height = 4,
Format = pixelFormat, Format = pixelFormat,
DefaultPaletteId = paletteId,
SourceData = sourceData, SourceData = sourceData,
}); });
} }

View file

@ -97,6 +97,8 @@ public static class ObjectMeshDataEquality {
Assert.True(expected.CullMode == actual.CullMode, $"{path}.CullMode: expected {expected.CullMode}, got {actual.CullMode}"); Assert.True(expected.CullMode == actual.CullMode, $"{path}.CullMode: expected {expected.CullMode}, got {actual.CullMode}");
Assert.True(expected.Translucency == actual.Translucency, Assert.True(expected.Translucency == actual.Translucency,
$"{path}.Translucency: expected {expected.Translucency}, got {actual.Translucency}"); $"{path}.Translucency: expected {expected.Translucency}, got {actual.Translucency}");
Assert.True(expected.MaterialState == actual.MaterialState,
$"{path}.MaterialState: expected {expected.MaterialState}, got {actual.MaterialState}");
Assert.True(expected.IsTransparent == actual.IsTransparent, $"{path}.IsTransparent: expected {expected.IsTransparent}, got {actual.IsTransparent}"); Assert.True(expected.IsTransparent == actual.IsTransparent, $"{path}.IsTransparent: expected {expected.IsTransparent}, got {actual.IsTransparent}");
Assert.True(expected.IsAdditive == actual.IsAdditive, $"{path}.IsAdditive: expected {expected.IsAdditive}, got {actual.IsAdditive}"); Assert.True(expected.IsAdditive == actual.IsAdditive, $"{path}.IsAdditive: expected {expected.IsAdditive}, got {actual.IsAdditive}");
Assert.True(expected.HasWrappingUVs == actual.HasWrappingUVs, $"{path}.HasWrappingUVs: expected {expected.HasWrappingUVs}, got {actual.HasWrappingUVs}"); Assert.True(expected.HasWrappingUVs == actual.HasWrappingUVs, $"{path}.HasWrappingUVs: expected {expected.HasWrappingUVs}, got {actual.HasWrappingUVs}");

View file

@ -192,6 +192,10 @@ public class ObjectMeshDataSerializerTests {
Indices = new List<ushort> { 0, 1, 2 }, Indices = new List<ushort> { 0, 1, 2 },
CullMode = CullMode.Clockwise, CullMode = CullMode.Clockwise,
Translucency = TranslucencyKind.Opaque, Translucency = TranslucencyKind.Opaque,
MaterialState = new RetailSetSurfaceMaterialState(
RetailSetSurfaceBlend.InverseAdditive,
RetailSetSurfaceAlphaTest.Paletted,
FogEnabled: false),
IsTransparent = false, IsTransparent = false,
IsAdditive = false, IsAdditive = false,
HasWrappingUVs = false, HasWrappingUVs = false,
@ -290,6 +294,37 @@ public class ObjectMeshDataSerializerTests {
BitConverter.SingleToInt32Bits(read.SurfaceOpacity)); BitConverter.SingleToInt32Bits(read.SurfaceOpacity));
} }
[Theory]
[InlineData(0, 0, true)]
[InlineData(1, 0, true)]
[InlineData(2, 1, true)]
[InlineData(3, 2, false)]
[InlineData(4, 0, true)]
[InlineData(5, 1, false)]
[InlineData(6, 2, true)]
public void SetSurfaceState_RoundTripsEveryPackedFieldDeterministically(
int blendValue,
int alphaTestValue,
bool fogEnabled) {
ObjectMeshData data = WithCellShellSubset();
TextureBatchData batch = Assert.Single(Assert.Single(data.TextureBatches).Value);
batch.MaterialState = new RetailSetSurfaceMaterialState(
(RetailSetSurfaceBlend)blendValue,
(RetailSetSurfaceAlphaTest)alphaTestValue,
fogEnabled);
using var first = new MemoryStream();
ObjectMeshDataSerializer.Write(data, first);
using var second = new MemoryStream();
ObjectMeshDataSerializer.Write(data, second);
Assert.Equal(first.ToArray(), second.ToArray());
TextureBatchData read = Assert.Single(Assert.Single(
ObjectMeshDataSerializer.Read(first.ToArray()).TextureBatches).Value);
Assert.Equal(batch.MaterialState, read.MaterialState);
Assert.Equal(batch.MaterialState.ToPackedByte(), read.MaterialState.ToPackedByte());
}
// ---- determinism ----------------------------------------------------- // ---- determinism -----------------------------------------------------
[Fact] [Fact]

View file

@ -10,13 +10,14 @@ public class PakFormatTests {
} }
/// <summary> /// <summary>
/// S5-c4: recipe 9 carries authored surface opacity in every prepared /// S5-c4 fix round 1: recipe 10 carries exact SetSurface state after
/// texture batch. It remains a payload recipe change, not pak framing. /// recipe 10's resolved SetSurface state after recipe 9's authored opacity.
/// It remains a payload recipe change.
/// </summary> /// </summary>
[Fact] [Fact]
public void FormatVersion_StaysAtTwo_AcrossTheS5C4RecipeBump() { public void FormatVersion_StaysAtTwo_AcrossTheS5C4RecipeBump() {
Assert.Equal(2u, PakFormat.CurrentFormatVersion); Assert.Equal(2u, PakFormat.CurrentFormatVersion);
Assert.Equal(9u, PakFormat.CurrentBakeToolVersion); Assert.Equal(10u, PakFormat.CurrentBakeToolVersion);
} }
[Fact] [Fact]

View file

@ -136,7 +136,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
} }
/// <summary> /// <summary>
/// S5-c4: an older recipe-8 package must be rejected by a recipe-9 /// S5-c4 fix round 1: an older recipe-9 package must be rejected by a recipe-10
/// consumer through the catalog-identity check /// consumer through the catalog-identity check
/// alone, BEFORE any TextureBatchData payload field is ever /// alone, BEFORE any TextureBatchData payload field is ever
/// deserialized -- so an older pak can never be silently misread as /// deserialized -- so an older pak can never be silently misread as
@ -160,7 +160,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
fs.Position = 36; fs.Position = 36;
Span<byte> buf = stackalloc byte[4]; Span<byte> buf = stackalloc byte[4];
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian( System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
buf, 8u); buf, 9u);
fs.Write(buf); fs.Write(buf);
} }
@ -168,7 +168,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
() => new PakPreparedAssetSource(path, Identity)); () => new PakPreparedAssetSource(path, Identity));
Assert.Contains("does not match the installed DAT set", error.Message); Assert.Contains("does not match the installed DAT set", error.Message);
Assert.Contains("Re-bake", error.Message); Assert.Contains("Re-bake", error.Message);
Assert.Contains("bake tool 8 != 9", error.Message); Assert.Contains("bake tool 9 != 10", error.Message);
} }
[Fact] [Fact]

View file

@ -337,6 +337,7 @@ public class EntityClassificationCacheTests
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
TextureLayer: 0, TextureLayer: 0,
Translucency: TranslucencyKind.Opaque, Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0u); FoliageFlags: 0u);
return new CachedBatch(key, new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), Matrix4x4.Identity); return new CachedBatch(key, new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), Matrix4x4.Identity);
} }

View file

@ -426,6 +426,7 @@ public sealed class WbDrawDispatcherBucketingTests
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
TextureLayer: 0, TextureLayer: 0,
Translucency: TranslucencyKind.Opaque, Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0u); FoliageFlags: 0u);
return new CachedBatch( return new CachedBatch(
key, key,

View file

@ -20,9 +20,9 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
// Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances) // Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances)
var groups = new List<WbDrawDispatcher.IndirectGroupInput> var groups = new List<WbDrawDispatcher.IndirectGroupInput>
{ {
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.75f), new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.75f),
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, SurfaceOpacity: 0.25f), new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.25f),
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.5f), new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.5f),
}; };
var indirect = new DrawElementsIndirectCommand[16]; var indirect = new DrawElementsIndirectCommand[16];
@ -75,12 +75,15 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
{ {
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0,
TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
CullMode: CullMode.Clockwise), CullMode: CullMode.Clockwise),
new(IndexCount: 20, FirstIndex: 10, BaseVertex: 0, InstanceCount: 1, FirstInstance: 1, new(IndexCount: 20, FirstIndex: 10, BaseVertex: 0, InstanceCount: 1, FirstInstance: 1,
TextureIndex: 0x2, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, TextureIndex: 0x2, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
CullMode: CullMode.None), CullMode: CullMode.None),
new(IndexCount: 30, FirstIndex: 30, BaseVertex: 0, InstanceCount: 1, FirstInstance: 2, new(IndexCount: 30, FirstIndex: 30, BaseVertex: 0, InstanceCount: 1, FirstInstance: 2,
TextureIndex: 0x3, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap, TextureIndex: 0x3, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
CullMode: CullMode.Landblock), CullMode: CullMode.Landblock),
}; };
var indirect = new DrawElementsIndirectCommand[4]; var indirect = new DrawElementsIndirectCommand[4];
@ -116,7 +119,7 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
// because the discard handles transparency, not blending. // because the discard handles transparency, not blending.
var groups = new List<WbDrawDispatcher.IndirectGroupInput> var groups = new List<WbDrawDispatcher.IndirectGroupInput>
{ {
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap), new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap, MaterialState: RetailSetSurfaceMaterialState.Opaque),
}; };
var indirect = new DrawElementsIndirectCommand[4]; var indirect = new DrawElementsIndirectCommand[4];
var batch = new WbDrawDispatcher.BatchDataPublic[4]; var batch = new WbDrawDispatcher.BatchDataPublic[4];
@ -146,7 +149,8 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
FirstInstance: index, FirstInstance: index,
TextureIndex: (uint)index, TextureIndex: (uint)index,
TextureLayer: 0, TextureLayer: 0,
Translucency: kind)).ToList(); Translucency: kind,
MaterialState: RetailSetSurfaceMaterialState.Opaque)).ToList();
var indirect = new DrawElementsIndirectCommand[kinds.Length]; var indirect = new DrawElementsIndirectCommand[kinds.Length];
var batches = new WbDrawDispatcher.BatchDataPublic[kinds.Length]; var batches = new WbDrawDispatcher.BatchDataPublic[kinds.Length];

View file

@ -77,4 +77,28 @@ public sealed class ContentMigrationCatalogTests
Assert.Contains("CellStruct", plan.Reason, StringComparison.OrdinalIgnoreCase); Assert.Contains("CellStruct", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase); Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
} }
[Fact]
public void RecipeNineToTenRequiresOneExplicitFullRebuild()
{
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(9, 10);
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
Assert.Equal(9u, plan.FromRecipeVersion);
Assert.Equal(10u, plan.TargetRecipeVersion);
Assert.Contains("SetSurface", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Empty(plan.EffectiveDatIds);
Assert.Empty(plan.EffectiveLandblocks);
}
[Fact]
public void AnyOlderRecipeToTenCollapsesToOneFullRebuild()
{
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(1, 10);
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
Assert.Equal(10u, plan.TargetRecipeVersion);
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Contains("SetSurface", plan.Reason, StringComparison.OrdinalIgnoreCase);
}
} }