From 15ed57a1e7f8cd4bad6fc145adca88e515cfcda1 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 5 Sep 2026 00:56:52 +0200 Subject: [PATCH] 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. --- .../retail-divergence-register.md | 14 + src/AcDream.App/Rendering/Gpu/GpuEnums.cs | 6 + .../Rendering/Gpu/Vk/VulkanViewportMapping.cs | 2 + .../Rendering/RetailDetailTextureContract.cs | 2 + .../Rendering/Shaders/mesh_atmospheric.frag | 7 +- .../Rendering/Shaders/mesh_modern.frag | 8 +- .../Shaders/spv/mesh_atmospheric.frag.spv | Bin 18828 -> 19320 bytes .../Shaders/spv/mesh_modern.frag.spv | Bin 5292 -> 5784 bytes .../Shaders/spv/shaders.manifest.json | 4 +- .../Rendering/Wb/EnvCellRenderer.Rhi.cs | 46 +++- .../Rendering/Wb/EnvCellRenderer.cs | 36 ++- src/AcDream.App/Rendering/Wb/GroupKey.cs | 1 + .../Rendering/Wb/ObjectMeshManager.cs | 3 + .../Wb/WbDrawDispatcher.OrderedStream.cs | 28 +- .../Rendering/Wb/WbDrawDispatcher.Rhi.cs | 89 ++++++- .../Wb/WbDrawDispatcher.WalkClassify.cs | 1 + .../Rendering/Wb/WbDrawDispatcher.cs | 12 +- src/AcDream.Content/MeshExtractor.cs | 12 + src/AcDream.Content/ObjectMeshData.cs | 7 + .../Pak/ObjectMeshDataSerializer.cs | 9 +- src/AcDream.Content/Pak/PakFormat.cs | 6 +- src/AcDream.Core/Meshing/TranslucencyKind.cs | 129 +++++++++ .../Installation/ContentMigrationCatalog.cs | 4 + .../LauncherInstallRecordStore.cs | 7 +- .../Rendering/EnvCellAlphaDrawSourceTests.cs | 252 ++++++++++++++---- .../Rendering/Gpu/GpuContractTests.cs | 4 +- .../Gpu/Vk/VulkanShaderManifestTests.cs | 4 +- .../Gpu/Vk/VulkanViewportMappingTests.cs | 6 + .../RetailDetailTextureContractTests.cs | 111 ++++++-- .../Walk/OrderPreservingSubmitterTests.cs | 149 ++++++++++- .../Rendering/Walk/OrderedDrawStreamTests.cs | 1 + .../Walk/WalkStaticStreamPopulatorTests.cs | 188 +++++++++++-- .../Rendering/Wb/EnvCellRendererTests.cs | 40 +-- .../Wb/FoliageWindClassificationTests.cs | 1 + .../Rendering/Wb/InstanceGroupClearTests.cs | 3 + .../MeshExtractorSolidFaceExtractionTests.cs | 59 +++- .../ObjectMeshDataEquality.cs | 2 + .../ObjectMeshDataSerializerTests.cs | 35 +++ tests/AcDream.Content.Tests/PakFormatTests.cs | 7 +- .../PreparedAssetSourceTests.cs | 6 +- .../Wb/EntityClassificationCacheTests.cs | 1 + .../Wb/WbDrawDispatcherBucketingTests.cs | 1 + .../WbDrawDispatcherIndirectBuilderTests.cs | 14 +- .../ContentMigrationCatalogTests.cs | 24 ++ 44 files changed, 1154 insertions(+), 187 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index fd4ad29b..5e61d740 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -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 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 | |---|---|---|---|---|---| +| 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 425083–425303; 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*) @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-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 | diff --git a/src/AcDream.App/Rendering/Gpu/GpuEnums.cs b/src/AcDream.App/Rendering/Gpu/GpuEnums.cs index b3b7db65..31ee76b3 100644 --- a/src/AcDream.App/Rendering/Gpu/GpuEnums.cs +++ b/src/AcDream.App/Rendering/Gpu/GpuEnums.cs @@ -132,6 +132,12 @@ internal enum GpuBlendMode /// Additive: SrcAlpha, One. Additive, + /// Retail raw Additive: One, One. + RawAdditive, + + /// Retail inverse-additive: OneMinusSrcAlpha, One. + InverseAdditive, + /// /// Retail's inverse-alpha translucency: OneMinusSrcAlpha, SrcAlpha. /// diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs index ac404025..c81ce023 100644 --- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs +++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanViewportMapping.cs @@ -176,6 +176,8 @@ internal static class VulkanViewportMapping GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha), GpuBlendMode.PremultipliedAlpha => (BlendFactor.One, BlendFactor.OneMinusSrcAlpha), 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. GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha), GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero), diff --git a/src/AcDream.App/Rendering/RetailDetailTextureContract.cs b/src/AcDream.App/Rendering/RetailDetailTextureContract.cs index 699f7753..55ca4739 100644 --- a/src/AcDream.App/Rendering/RetailDetailTextureContract.cs +++ b/src/AcDream.App/Rendering/RetailDetailTextureContract.cs @@ -34,6 +34,8 @@ namespace AcDream.App.Rendering; /// internal static class RetailDetailTextureContract { + internal const int NoFogRenderPassFlag = 0x200; + internal static bool ShouldRender( bool settingEnabled, TerrainAtlas.RetailDetailTextureBinding binding) => diff --git a/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag b/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag index 65b81ebb..3eed52e6 100644 --- a/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag +++ b/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag @@ -128,9 +128,12 @@ void main() { alpha = color.a * vOpacityMultiplier; } - if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff) + if (detailActive + ? isRetailClipReference(uParamB) && alpha < uParamB + : color.a < alphaCutoff) discard; - rgb = applyFog(rgb, vWorldPos); + if (!detailActive || (uRenderPass & 0x200) == 0) + rgb = applyFog(rgb, vWorldPos); FragColor = vec4(rgb, alpha); } diff --git a/src/AcDream.App/Rendering/Shaders/mesh_modern.frag b/src/AcDream.App/Rendering/Shaders/mesh_modern.frag index 291758a1..804c669b 100644 --- a/src/AcDream.App/Rendering/Shaders/mesh_modern.frag +++ b/src/AcDream.App/Rendering/Shaders/mesh_modern.frag @@ -25,6 +25,7 @@ in flat uint vDetailCategory; // discard alpha >= 0.95 (already drawn opaque) and // alpha < 0.05 (skip empty fragments — large // transparent overdraw cost otherwise) +// bit 0x200 = detail-active raw Additive; bypass fixed-function fog uniform int uRenderPass; 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 @@ -120,9 +121,12 @@ void main() { // '<' spells D3DCMP_GREATEREQUAL: equality passes. Detail-active CLIP // 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; - rgb = applyFog(rgb, vWorldPos); + if (!detailActive || (uRenderPass & 0x200) == 0) + rgb = applyFog(rgb, vWorldPos); FragColor = vec4(rgb, alpha); } diff --git a/src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/mesh_atmospheric.frag.spv index 258dd8c2abbd86d6fffcbf15ff3caaf76b611aa0..0d734a18622d316114d5373eecd42417cf42cdc2 100644 GIT binary patch literal 19320 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 JGkj+1{{_A?4z>UQ literal 18828 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 delta 1408 zcmYk5%}bO~6vodxA3EyDh&G6{$Z%nzeGpLtR-`sA+LW4+W{V67BB#}C0(mjBlFHHx z6@8#mK~@{F0b8VhLG(Ygjh3MmLC*ax2R{@gTSQBYr(m)oBU)Y-TxEOtm)U15u4X7Pn#9Uin)VvtCz9ZHE2w-b=k9 zq^&|fvjJ&LN(jW)ENM;RXT^z4h`&`7ZBRb?rmA0%W&BnQ{hWYCH~5t5Q2)F*J*UiE zdv(>tA^G~3`w{c~hQ*1|@0V)$TG*dfS?)g~PJebqK7E#>eXNU*tj<-p1i`v1)ocX$ z)nfX4EEsi{G7Z_wie`jS;k2bx&uq-iWOjvDq>KqVv-nOK1PR)V2*1GBBn)HHOscTGr6f3$fb*+O3#K>uv z^=GtSoDB9#oeo)Y$nA@I9TZE=?%L{ih9xm7QJDy{xzQuC3~rR1!vY!| z;E8Nd?&>w{G39b%N5yjjb>7G_{2QE)X5zuG|=a^U!K6;mhNgL3FdvaJnzd-XU+56Ja{R?>@ zjs_T*n82H0*RvAXIeGqjdMK{Hbw_dSxV+&<63ZKIR)9~_?|4oDfu>D>@o`x_^J*<; z`{cWGX8)p`||6dB1!}m@8Cvoz) zIOcL;{#~CCC(iI%*S0oPZsNu6D3Oagqq1CVk2DG}fjA%0alM|eQH%xUrSVFze^oIj F`~wtAthWFF diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json index 7e8a63c0..a5277b73 100644 --- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json +++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json @@ -220,7 +220,7 @@ }, { "stage": "frag", - "sourceSha256": "5eb058e92ab27f1f6c245401e05f78a278fe00696b5c77f36ae33f8428f83bf3", + "sourceSha256": "c95e372c557a8a4b252ecf824ad049fe9611b0a1e49b0e6d18938d373d9c6794", "compiled": true } ] @@ -236,7 +236,7 @@ }, { "stage": "frag", - "sourceSha256": "396c382af73505bc2cac7a2db5878a63166730e47096a94630c35ad8376fe565", + "sourceSha256": "e145f32cbd50edcbc5e3196a49f0e2b05548bc0199ce13139e01c443794473b0", "compiled": true } ] diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs index e86aa34b..9a7e0232 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs @@ -2,6 +2,7 @@ using System.Numerics; using System.Runtime.InteropServices; using AcDream.App.Rendering.Gpu; using AcDream.Core.Lighting; +using AcDream.Core.Meshing; using DatReaderWriter.Enums; namespace AcDream.App.Rendering.Wb; @@ -28,6 +29,9 @@ public sealed unsafe partial class EnvCellRenderer private IGpuPipeline? _alphaPipeline; private IGpuPipeline? _clipPipeline; private IGpuPipeline? _additivePipeline; + private IGpuPipeline? _rawAdditivePipeline; + private IGpuPipeline? _inversePipeline; + private IGpuPipeline? _inverseAdditivePipeline; private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail; private readonly Func _buildingDetailEnabled; @@ -48,7 +52,7 @@ public sealed unsafe partial class EnvCellRenderer /// /// The RHI arm's constructor. It also completes Initialize'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 Shader to hand in. /// internal EnvCellRenderer( @@ -80,6 +84,12 @@ public sealed unsafe partial class EnvCellRenderer scope.SampleCount); _additivePipeline = CreateShellPipeline( 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; } @@ -272,7 +282,9 @@ public sealed unsafe partial class EnvCellRenderer : isAdditive ? _additivePipeline! : _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 // to re-establish the mesh, which is vertex-array state. @@ -289,10 +301,14 @@ public sealed unsafe partial class EnvCellRenderer pushConstants.RenderPass = isAdditive ? (int)renderPass | 0x100 : (int)renderPass; + if (detailEnabled && !drawRange.MaterialState.FogEnabled) + pushConstants.RenderPass |= RetailDetailTextureContract.NoFogRenderPassFlag; pushConstants.DrawIdOffset = drawRange.FirstCommand; - pushConstants.ParamB = isClip - ? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f - : 0f; + pushConstants.ParamB = detailEnabled + ? drawRange.MaterialState.AlphaTestReference + : isClip + ? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f + : 0f; pushConstants.TextureIndexA = detailEnabled ? _environmentDetail.TextureSlot.Index : 0u; @@ -311,9 +327,23 @@ public sealed unsafe partial class EnvCellRenderer pushConstants.TextureIndexA = 0; pushConstants.ParamA = 0f; pushConstants.ParamB = 0f; + pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag; 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( IGpuPassEncoder encoder, IGpuPipeline pipeline, @@ -429,5 +459,11 @@ public sealed unsafe partial class EnvCellRenderer _clipPipeline = null; _additivePipeline?.Dispose(); _additivePipeline = null; + _rawAdditivePipeline?.Dispose(); + _rawAdditivePipeline = null; + _inversePipeline?.Dispose(); + _inversePipeline = null; + _inverseAdditivePipeline?.Dispose(); + _inverseAdditivePipeline = null; } } diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index a397d0df..ec82fdfd 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -24,6 +24,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using AcDream.Core.Meshing; using DatReaderWriter.Enums; namespace AcDream.App.Rendering.Wb; @@ -162,7 +163,11 @@ public sealed partial class EnvCellRenderer : private readonly List _mdiDrawRanges = new(); 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 // global grouping lazily instead of duplicating every prepared gameplay // instance in both a per-cell and global tree each frame. @@ -883,7 +888,7 @@ public sealed partial class EnvCellRenderer : EnvCellTransparentRoute transparentRoute, 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 // answers whether this renderer is ready to draw. if (!_initialized) return; @@ -1304,7 +1309,6 @@ public sealed partial class EnvCellRenderer : int groupIndex = _activeCullGroups[activeIndex]; List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group = _batchesByCullGroup[groupIndex]; - int firstCommand = cmdIndex; foreach (var item in group) { _modernBatches[cmdIndex] = new ModernBatchData @@ -1330,22 +1334,14 @@ public sealed partial class EnvCellRenderer : BaseVertex = (int)item.batch.BaseVertex, BaseInstance = (uint)item.instanceOffset, }; + AppendMdiDrawRange( + _mdiDrawRanges, + groupIndex, + cmdIndex, + 1, + item.batch.MaterialState); 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( - _mdiDrawRanges, - groupIndex, - firstCommand, - commandCount); } } @@ -1400,7 +1396,8 @@ public sealed partial class EnvCellRenderer : List ranges, int groupIndex, int firstCommand, - int commandCount) + int commandCount, + RetailSetSurfaceMaterialState materialState) { ArgumentNullException.ThrowIfNull(ranges); if (commandCount <= 0) @@ -1410,6 +1407,7 @@ public sealed partial class EnvCellRenderer : { MdiDrawRange previous = ranges[^1]; if (previous.GroupIndex == groupIndex + && previous.MaterialState == materialState && previous.FirstCommand + previous.CommandCount == firstCommand) { 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)); } // --------------------------------------------------------------------------- diff --git a/src/AcDream.App/Rendering/Wb/GroupKey.cs b/src/AcDream.App/Rendering/Wb/GroupKey.cs index da5fccfe..a1cd2a99 100644 --- a/src/AcDream.App/Rendering/Wb/GroupKey.cs +++ b/src/AcDream.App/Rendering/Wb/GroupKey.cs @@ -51,6 +51,7 @@ internal readonly record struct GroupKey( GpuTextureSlot TextureSlot, uint TextureLayer, TranslucencyKind Translucency, + RetailSetSurfaceMaterialState MaterialState, uint FoliageFlags, float SurfaceOpacity = 1f, CullMode CullMode = CullMode.CounterClockwise); diff --git a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs index aad48c6e..95b3b425 100644 --- a/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs +++ b/src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs @@ -120,6 +120,8 @@ namespace AcDream.App.Rendering.Wb public TextureKey Key { get; set; } public DatReaderWriter.Enums.CullMode CullMode { 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 IsAdditive { get; set; } public bool HasWrappingUVs { get; set; } @@ -2262,6 +2264,7 @@ namespace AcDream.App.Rendering.Wb TextureFormat = format.Format, RetailSurfaceMask = batch.RetailSurfaceMask, Translucency = batch.Translucency, + MaterialState = batch.MaterialState, IsTransparent = batch.IsTransparent, IsAdditive = batch.IsAdditive, HasWrappingUVs = batch.HasWrappingUVs, diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs index da960b03..df8bdd53 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs @@ -488,7 +488,14 @@ public sealed unsafe partial class WbDrawDispatcher if (global is null) 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 { 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 // across calls. Between ordered ranges the walk's leaf draws run // (terrain, cell shells, sky, punch fans) and RetailAlphaQueue @@ -521,6 +523,7 @@ public sealed unsafe partial class WbDrawDispatcher // DrawPreparedAlphaBatchRhi does for the same reason. BindPipelineWithMesh(encoder, pipelines.Opaque, global); encoder.SetPushConstants(in pushConstants); + BindDirectionalShadowReceiver(encoder, in shadowBinding); BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances); BindSection(encoder, GpuBindingModel.StorageBatches, _orderedBatches); BindSection(encoder, GpuBindingModel.StorageClipSlots, _orderedClipSlots); @@ -567,11 +570,16 @@ public sealed unsafe partial class WbDrawDispatcher ValidateMergeRun(_orderedStream, run); 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; - if (detailEnabled - && _orderedStream.DetailCategories[run.FirstCommand] != 0u) - ArmBuildingDetail(ref pushConstants); + if (hasDetail) + ArmBuildingDetail(ref pushConstants, key.MaterialState); else ClearDetailPushConstants(ref pushConstants); diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs index b4091101..ffba9af9 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs @@ -52,7 +52,9 @@ public sealed unsafe partial class WbDrawDispatcher IGpuPipeline OpaqueAlphaToCoverage, IGpuPipeline AlphaBlend, IGpuPipeline AlphaAdditive, - IGpuPipeline AlphaInverse); + IGpuPipeline RawAdditive, + IGpuPipeline AlphaInverse, + IGpuPipeline InverseAdditive); private MeshPipelineSet? _backbufferPipelines; private MeshPipelineSet? _offscreenPipelines; @@ -227,7 +229,7 @@ public sealed unsafe partial class WbDrawDispatcher bool usesRenderPackShaderAbi = false) { string suffix = samples > 1 ? string.Empty : "-1x"; - var created = new List(5); + var created = new List(7); try { return new MeshPipelineSet( @@ -252,10 +254,20 @@ public sealed unsafe partial class WbDrawDispatcher shaders: baseShaders, shaderName: baseShaderName, usesRenderPackShaderAbi: usesRenderPackShaderAbi)), + Track(CreateMeshPipeline( + device, $"{namePrefix}-raw-additive{suffix}", GpuBlendMode.RawAdditive, false, false, samples, + shaders: baseShaders, + shaderName: baseShaderName, + usesRenderPackShaderAbi: usesRenderPackShaderAbi)), Track(CreateMeshPipeline( device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples, shaders: baseShaders, shaderName: baseShaderName, + usesRenderPackShaderAbi: usesRenderPackShaderAbi)), + Track(CreateMeshPipeline( + device, $"{namePrefix}-inverse-additive{suffix}", GpuBlendMode.InverseAdditive, false, false, samples, + shaders: baseShaders, + shaderName: baseShaderName, usesRenderPackShaderAbi: usesRenderPackShaderAbi))); } catch @@ -370,9 +382,12 @@ public sealed unsafe partial class WbDrawDispatcher encoder, frame, out DirectionalShadowFrameBinding shadowBinding); + IGpuPipeline opaquePipeline = AlphaToCoverage + ? pipelines.OpaqueAlphaToCoverage + : pipelines.Opaque; BindPipelineWithMesh( encoder, - AlphaToCoverage ? pipelines.OpaqueAlphaToCoverage : pipelines.Opaque, + opaquePipeline, mesh); encoder.SetPushConstants(in pushConstants); BindDirectionalShadowReceiver(encoder, in shadowBinding); @@ -436,7 +451,8 @@ public sealed unsafe partial class WbDrawDispatcher using (BeginRhiTimer(encoder, diag, OpaqueTimerScope)) { DrawDetailAwareRangeRhi( - encoder, ref pushConstants, commandBuffer, commandBase, + encoder, mesh, pipelines, opaquePipeline, + ref pushConstants, commandBuffer, commandBase, 0, _opaqueDrawCount, usedDetailCategories, detailEnabled); } } @@ -634,7 +650,10 @@ public sealed unsafe partial class WbDrawDispatcher /// (M3c) precisely because detail-eligible content no longer reaches it. /// private void DrawImmediateAlphaInstanceRhi( - GlobalMeshBuffer mesh, TranslucencyKind blend, Matrix4x4 viewProjection) + GlobalMeshBuffer mesh, + TranslucencyKind blend, + RetailSetSurfaceMaterialState materialState, + Matrix4x4 viewProjection) { if (_alphaCommands.Buffer is null) return; @@ -644,9 +663,12 @@ public sealed unsafe partial class WbDrawDispatcher GpuPushConstants pushConstants = BindAlphaDrawState( 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); - ArmBuildingDetail(ref pushConstants); + ArmBuildingDetail(ref pushConstants, materialState); DrawIndirectRangeRhi( encoder, ref pushConstants, @@ -673,6 +695,8 @@ public sealed unsafe partial class WbDrawDispatcher while (command < end) { TranslucencyKind blend = _groupInputScratch[command].Translucency; + RetailSetSurfaceMaterialState materialState = + _groupInputScratch[command].MaterialState; bool hasDetail = detailEnabled && CommandContainsDetailCategory( _indirectCommands[command], @@ -691,10 +715,12 @@ public sealed unsafe partial class WbDrawDispatcher BindPipelineWithMesh( encoder, - PipelineForBlend(pipelines, blend), + hasDetail + ? PipelineForMaterial(pipelines, materialState, pipelines.Opaque) + : PipelineForBlend(pipelines, blend), mesh); if (hasDetail) - ArmBuildingDetail(ref pushConstants); + ArmBuildingDetail(ref pushConstants, materialState); else ClearDetailPushConstants(ref pushConstants); DrawIndirectRangeRhi( @@ -712,6 +738,9 @@ public sealed unsafe partial class WbDrawDispatcher private void DrawDetailAwareRangeRhi( IGpuPassEncoder encoder, + GlobalMeshBuffer mesh, + MeshPipelineSet pipelines, + IGpuPipeline opaquePipeline, ref GpuPushConstants pushConstants, IGpuBuffer commandBuffer, uint commandBase, @@ -738,9 +767,20 @@ public sealed unsafe partial class WbDrawDispatcher } if (hasDetail) - ArmBuildingDetail(ref pushConstants); + { + RetailSetSurfaceMaterialState materialState = + _groupInputScratch[command].MaterialState; + BindPipelineWithMesh( + encoder, + PipelineForMaterial(pipelines, materialState, opaquePipeline), + mesh); + ArmBuildingDetail(ref pushConstants, materialState); + } else + { + BindPipelineWithMesh(encoder, opaquePipeline, mesh); ClearDetailPushConstants(ref pushConstants); + } DrawIndirectRangeRhi( encoder, ref pushConstants, @@ -754,10 +794,17 @@ public sealed unsafe partial class WbDrawDispatcher encoder.SetPushConstants(in pushConstants); } - private void ArmBuildingDetail(ref GpuPushConstants pushConstants) + private void ArmBuildingDetail( + ref GpuPushConstants pushConstants, + RetailSetSurfaceMaterialState materialState) { pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index; pushConstants.ParamA = _buildingDetail.Tiling; + pushConstants.ParamB = materialState.AlphaTestReference; + if (materialState.FogEnabled) + pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag; + else + pushConstants.RenderPass |= RetailDetailTextureContract.NoFogRenderPassFlag; } private static void ClearDetailPushConstants( @@ -766,6 +813,7 @@ public sealed unsafe partial class WbDrawDispatcher pushConstants.TextureIndexA = 0; pushConstants.ParamA = 0f; pushConstants.ParamB = 0f; + pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag; } private static IGpuPipeline PipelineForBlend(MeshPipelineSet pipelines, TranslucencyKind blend) => @@ -776,6 +824,23 @@ public sealed unsafe partial class WbDrawDispatcher _ => 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."), + }; + /// /// Reads cull modes from when the caller /// supplies one, or from the shared scratch @@ -1102,7 +1167,9 @@ public sealed unsafe partial class WbDrawDispatcher pipelines.OpaqueAlphaToCoverage.Dispose(); pipelines.AlphaBlend.Dispose(); pipelines.AlphaAdditive.Dispose(); + pipelines.RawAdditive.Dispose(); pipelines.AlphaInverse.Dispose(); + pipelines.InverseAdditive.Dispose(); } private sealed class NullRhiTimerScope : IDisposable diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs index e9dbdba8..a36fc067 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs @@ -240,6 +240,7 @@ public sealed partial class WbDrawDispatcher key = new GroupKey( batch.FirstIndex, (int)batch.BaseVertex, batch.IndexCount, texture.Slot, texture.Layer, translucency, + MaterialState: batch.MaterialState, FoliageFlags: foliageFlags, SurfaceOpacity: batch.SurfaceOpacity, CullMode: batch.CullMode); diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index dd34acb5..68cb51e6 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -2050,6 +2050,7 @@ public sealed partial class WbDrawDispatcher : IDisposable TextureIndex: g.TextureSlot.Index, TextureLayer: g.TextureLayer, Translucency: g.Translucency, + MaterialState: g.MaterialState, SurfaceOpacity: g.SurfaceOpacity, CullMode: g.CullMode, FoliageFlags: g.FoliageFlags); @@ -2123,6 +2124,7 @@ public sealed partial class WbDrawDispatcher : IDisposable g.TextureSlot, g.TextureLayer, g.Translucency, + g.MaterialState, g.FoliageFlags, g.SurfaceOpacity, g.CullMode); @@ -2144,6 +2146,7 @@ public sealed partial class WbDrawDispatcher : IDisposable TextureSlot = key.TextureSlot, TextureLayer = key.TextureLayer, Translucency = key.Translucency, + MaterialState = key.MaterialState, CullMode = key.CullMode, FoliageFlags = key.FoliageFlags, SurfaceOpacity = key.SurfaceOpacity, @@ -2641,7 +2644,11 @@ public sealed partial class WbDrawDispatcher : IDisposable EnsureDeferredAlphaCapacity(1); WriteDeferredAlphaEntrySlot(0, in entry); PrepareRhiAlphaSections(1); - DrawImmediateAlphaInstanceRhi(global, entry.Key.Translucency, viewProjection); + DrawImmediateAlphaInstanceRhi( + global, + entry.Key.Translucency, + entry.Key.MaterialState, + viewProjection); } private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) @@ -3437,6 +3444,7 @@ public sealed partial class WbDrawDispatcher : IDisposable uint TextureIndex, uint TextureLayer, TranslucencyKind Translucency, + RetailSetSurfaceMaterialState MaterialState, float SurfaceOpacity = 1f, CullMode CullMode = CullMode.CounterClockwise, // Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag / @@ -3675,6 +3683,8 @@ public sealed partial class WbDrawDispatcher : IDisposable AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned; public uint TextureLayer; // Layer in either the pooled composite array or WB shared atlas. public TranslucencyKind Translucency; + public RetailSetSurfaceMaterialState MaterialState = + RetailSetSurfaceMaterialState.Opaque; public float SurfaceOpacity = 1f; public CullMode CullMode; public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes) diff --git a/src/AcDream.Content/MeshExtractor.cs b/src/AcDream.Content/MeshExtractor.cs index 9d6a0c78..dbbbd36f 100644 --- a/src/AcDream.Content/MeshExtractor.cs +++ b/src/AcDream.Content/MeshExtractor.cs @@ -396,6 +396,7 @@ public sealed class MeshExtractor { bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type); bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap); uint paletteId = 0; + bool texturePresent = false; bool isDxt3or5 = false; bool textureDataIsCached = false; DatReaderWriter.Enums.PixelFormat? sourceFormat = null; @@ -410,6 +411,7 @@ public sealed class MeshExtractor { textureDataIsCached = true; } else if (_dats.Portal.TryGet(surface.OrigTextureId, out var surfaceTexture)) { + texturePresent = true; var renderSurfaceId = surfaceTexture.Textures.First(); if (!_dats.Portal.TryGet(renderSurfaceId, out var renderSurface)) { // check highres @@ -603,6 +605,10 @@ public sealed class MeshExtractor { TranslucencyKindExtensions.OpacityFromSurfaceTranslucency( surface.Type, surface.Translucency), + MaterialState = RetailSetSurfaceMaterialState.Resolve( + surface.Type, + texturePresent, + paletteId != 0), IsTransparent = isTransparent, IsAdditive = isAdditive }; @@ -900,6 +906,7 @@ public sealed class MeshExtractor { bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type); bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap); uint paletteId = 0; + bool texturePresent = false; bool isDxt3or5 = false; bool textureDataIsCached = false; DatReaderWriter.Enums.PixelFormat? sourceFormat = null; @@ -914,6 +921,7 @@ public sealed class MeshExtractor { textureDataIsCached = true; } else if (_dats.Portal.TryGet(surface.OrigTextureId, out var surfaceTexture)) { + texturePresent = true; var renderSurfaceId = surfaceTexture.Textures.First(); if (!_dats.Portal.TryGet(renderSurfaceId, out var renderSurface)) { if (!_dats.HighRes.TryGet(renderSurfaceId, out var hrRenderSurface)) { @@ -1080,6 +1088,10 @@ public sealed class MeshExtractor { TranslucencyKindExtensions.OpacityFromSurfaceTranslucency( surface.Type, surface.Translucency), + MaterialState = RetailSetSurfaceMaterialState.Resolve( + surface.Type, + texturePresent, + paletteId != 0), IsTransparent = isTransparent, IsAdditive = isAdditive, }; diff --git a/src/AcDream.Content/ObjectMeshData.cs b/src/AcDream.Content/ObjectMeshData.cs index 1966df3f..84899cfa 100644 --- a/src/AcDream.Content/ObjectMeshData.cs +++ b/src/AcDream.Content/ObjectMeshData.cs @@ -181,6 +181,13 @@ public class TextureBatchData { /// public float SurfaceOpacity { get; set; } = 1f; + /// + /// Exact retail SetSurface blend/alpha-test/fog state resolved during + /// extraction from the source Surface and texture palette class. + /// + public RetailSetSurfaceMaterialState MaterialState { get; set; } = + RetailSetSurfaceMaterialState.Opaque; + /// /// Retail source surface-array index this subset was constructed from /// (contract §3.6) — the CellStruct subset/material OWNER, not the diff --git a/src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs b/src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs index f8e740ca..85de9c2f 100644 --- a/src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs +++ b/src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs @@ -228,9 +228,13 @@ public static class ObjectMeshDataSerializer { w.Write(batch.RetailSurfaceMask); w.Write(batch.RawSurfaceType); 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. 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( @@ -256,6 +260,9 @@ public static class ObjectMeshDataSerializer { batch.RawSurfaceType = r.ReadUInt32(); batch.IsCellShell = r.ReadBoolean(); batch.SurfaceOpacity = r.ReadSingle(); + batch.MaterialState = + AcDream.Core.Meshing.RetailSetSurfaceMaterialState.FromPackedByte( + r.ReadByte()); return batch; } diff --git a/src/AcDream.Content/Pak/PakFormat.cs b/src/AcDream.Content/Pak/PakFormat.cs index 43171d9f..663975a2 100644 --- a/src/AcDream.Content/Pak/PakFormat.cs +++ b/src/AcDream.Content/Pak/PakFormat.cs @@ -45,9 +45,11 @@ public static class PakFormat { /// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after /// surface resolution. Version 9 appends authored surface opacity to /// 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. /// - public const uint CurrentBakeToolVersion = 9; + public const uint CurrentBakeToolVersion = 10; } /// diff --git a/src/AcDream.Core/Meshing/TranslucencyKind.cs b/src/AcDream.Core/Meshing/TranslucencyKind.cs index d4ab468c..2927dc15 100644 --- a/src/AcDream.Core/Meshing/TranslucencyKind.cs +++ b/src/AcDream.Core/Meshing/TranslucencyKind.cs @@ -38,6 +38,135 @@ public enum TranslucencyKind InvAlpha = 4, } +/// +/// Exact framebuffer blend selected by retail's +/// D3DPolyRender::SetSurface. This is deliberately separate from +/// , which remains the existing queue-membership +/// classification and cannot distinguish the additive factor pairs. +/// +public enum RetailSetSurfaceBlend : byte +{ + Opaque = 0, + StraightAlpha = 1, + AlphaAdditive = 2, + Additive = 3, + InverseAlpha = 4, + InverseAdditive = 5, + Clip = 6, +} + +/// The exact final-alpha comparison selected by SetSurface. +public enum RetailSetSurfaceAlphaTest : byte +{ + Disabled = 0, + Paletted = 1, + Dds = 2, +} + +/// +/// 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. +/// +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), + }; + + /// One deterministic recipe byte; bits 6-7 remain reserved. + 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 { // Translucent override comes FIRST, then the existing priority chain: diff --git a/src/AcDream.Launcher.Core/Installation/ContentMigrationCatalog.cs b/src/AcDream.Launcher.Core/Installation/ContentMigrationCatalog.cs index 3dae6459..6d236a26 100644 --- a/src/AcDream.Launcher.Core/Installation/ContentMigrationCatalog.cs +++ b/src/AcDream.Launcher.Core/Installation/ContentMigrationCatalog.cs @@ -61,6 +61,10 @@ public static class ContentMigrationCatalog 8, 9, "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) diff --git a/src/AcDream.Launcher.Core/Installation/LauncherInstallRecordStore.cs b/src/AcDream.Launcher.Core/Installation/LauncherInstallRecordStore.cs index 349ef622..cff72adb 100644 --- a/src/AcDream.Launcher.Core/Installation/LauncherInstallRecordStore.cs +++ b/src/AcDream.Launcher.Core/Installation/LauncherInstallRecordStore.cs @@ -39,9 +39,10 @@ public sealed class LauncherInstallRecordStore // with retail's authored DrawingBSP view sphere. Version 8 (OH2/S1) keeps // pak format 2 and regenerates every CellStruct/EnvCell render record // with retail's exact surface-array-index subset construction. Version 9 - // carries authored surface opacity in prepared mesh batches. Each is a - // mandatory full rebuild from its predecessor recipe. - public const uint CurrentBakeToolVersion = 9; + // carries authored surface opacity in prepared mesh batches. Version 10 + // adds exact resolved SetSurface state. Each is a mandatory full rebuild + // from its predecessor recipe. + public const uint CurrentBakeToolVersion = 10; private static readonly JsonSerializerOptions SerializerOptions = new() { diff --git a/tests/AcDream.App.Tests/Rendering/EnvCellAlphaDrawSourceTests.cs b/tests/AcDream.App.Tests/Rendering/EnvCellAlphaDrawSourceTests.cs index 3d5b7b6f..42f6c925 100644 --- a/tests/AcDream.App.Tests/Rendering/EnvCellAlphaDrawSourceTests.cs +++ b/tests/AcDream.App.Tests/Rendering/EnvCellAlphaDrawSourceTests.cs @@ -8,7 +8,9 @@ using AcDream.App.Rendering.Gpu.Vk; using AcDream.App.Rendering.Wb; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; +using AcDream.Core.Meshing; using Chorizite.Core.Render.Enums; +using DatReaderWriter.Enums; using Microsoft.Extensions.Logging.Abstractions; using CullMode = DatReaderWriter.Enums.CullMode; @@ -21,6 +23,11 @@ namespace AcDream.App.Tests.Rendering; /// 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); + /// /// Canonical F4180104 surface 08000BFF's pure Base1ClipMap mask 0x08 /// reaches CLIP while alpha-family 0x02 reaches ALPHA. Mutation check: @@ -75,26 +82,45 @@ public sealed class EnvCellAlphaDrawSourceTests DrawPipelineNames(fixture.Device)); } - [Theory] - [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) + public static TheoryData ExactMaterialRows() => new() { - BatchSpec spec = specIndex switch - { - 0 => BatchSpec.Opaque, - 1 => BatchSpec.Alpha, - 2 => BatchSpec.Additive, - 3 => BatchSpec.ClipDds, - 4 => BatchSpec.ClipPaletted, - _ => throw new ArgumentOutOfRangeException(nameof(specIndex)), - }; + { SurfaceType.Base1Image, false, "envcell-opaque", GpuBlendMode.None, 0f, true }, + { SurfaceType.Base1Image | SurfaceType.Alpha, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, true }, + { SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false, "envcell-additive", GpuBlendMode.Additive, 0f, false }, + { SurfaceType.Base1Image | SurfaceType.Additive, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 0f, false }, + { SurfaceType.Base1Image | SurfaceType.InvAlpha, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 0f, true }, + { SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive, false, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 0f, false }, + { SurfaceType.Translucent, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, true }, + { 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( detailSurfaceActive: true, spec); @@ -113,6 +139,20 @@ public sealed class EnvCellAlphaDrawSourceTests DrawPushConstants(fixture.Device), constants => constants.ParamA != 0f); 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); GpuRecordedStorageBind batchBind = fixture.Device.Calls .OfType() @@ -120,10 +160,77 @@ public sealed class EnvCellAlphaDrawSourceTests ReadOnlySpan gpuBatches = MemoryMarshal.Cast( fixture.Device.RingBytes.Slice((int)batchBind.OffsetBytes, (int)batchBind.SizeBytes)); 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(); Assert.Single(fixture.Device.Calls.OfType()); } + public static TheoryData 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()); + 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); + } + /// Two cell tokens from the SAME , /// with an unrelated source's entry appended between them, must still /// 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 }) { Assert.Contains( - "if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)", + "? isRetailClipReference(uParamB) && alpha < uParamB", shader, StringComparison.Ordinal); + Assert.Contains("(uRenderPass & 0x200) == 0", shader, StringComparison.Ordinal); Assert.DoesNotContain("alpha <= alphaCutoff", shader, StringComparison.Ordinal); Assert.DoesNotContain("color.a <= alphaCutoff", shader, StringComparison.Ordinal); Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", shader, StringComparison.Ordinal); @@ -409,6 +517,38 @@ public sealed class EnvCellAlphaDrawSourceTests 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() { DirectoryInfo? cursor = new(AppContext.BaseDirectory); @@ -419,46 +559,32 @@ public sealed class EnvCellAlphaDrawSourceTests } private readonly record struct BatchSpec( - byte Mask, - bool IsTransparent, - AcDream.Core.Meshing.TranslucencyKind Translucency, - uint PaletteId) + SurfaceType Type, + bool Paletted, + bool PositiveStippling) { internal static BatchSpec Opaque { get; } = new( - 0x00, - IsTransparent: false, - AcDream.Core.Meshing.TranslucencyKind.Opaque, - PaletteId: 0); + SurfaceType.Base1Image, Paletted: false, PositiveStippling: false); internal static BatchSpec ClipDds { get; } = new( - RetailAlphaMeshRouter.MaskClipMap, - IsTransparent: true, - AcDream.Core.Meshing.TranslucencyKind.ClipMap, - PaletteId: 0); + SurfaceType.Base1Image | SurfaceType.Base1ClipMap, + Paletted: false, PositiveStippling: false); internal static BatchSpec ClipPaletted { get; } = new( - RetailAlphaMeshRouter.MaskClipMap, - IsTransparent: true, - AcDream.Core.Meshing.TranslucencyKind.ClipMap, - PaletteId: 0x04000001u); + SurfaceType.Base1Image | SurfaceType.Base1ClipMap, + Paletted: true, PositiveStippling: false); internal static BatchSpec ClipPositiveStippleDds { get; } = new( - RetailAlphaMeshRouter.MaskClipMap | RetailAlphaMeshRouter.MaskPositiveStipple, - IsTransparent: true, - AcDream.Core.Meshing.TranslucencyKind.ClipMap, - PaletteId: 0); + SurfaceType.Base1Image | SurfaceType.Base1ClipMap, + Paletted: false, PositiveStippling: true); internal static BatchSpec Alpha { get; } = new( - RetailAlphaMeshRouter.MaskAlphaFamily, - IsTransparent: true, - AcDream.Core.Meshing.TranslucencyKind.AlphaBlend, - PaletteId: 0); + SurfaceType.Base1Image | SurfaceType.Alpha, + Paletted: false, PositiveStippling: false); internal static BatchSpec Additive { get; } = new( - RetailAlphaMeshRouter.MaskAlphaFamily, - IsTransparent: true, - AcDream.Core.Meshing.TranslucencyKind.Additive, - PaletteId: 0); + SurfaceType.Base1Image | SurfaceType.Additive, + Paletted: false, PositiveStippling: false); } /// @@ -512,27 +638,45 @@ public sealed class EnvCellAlphaDrawSourceTests for (int i = 0; i < specs.Length; 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 { Key = new TextureKey { SurfaceId = 0x08000BFFu + (uint)i, - PaletteId = spec.PaletteId, + PaletteId = paletteId, }, TextureData = new byte[8 * 8 * 4], Indices = [0, 1, 2], - IsTransparent = spec.IsTransparent, - IsAdditive = spec.Translucency == AcDream.Core.Meshing.TranslucencyKind.Additive, - Translucency = spec.Translucency, + IsTransparent = translucency != TranslucencyKind.Opaque, + IsAdditive = (spec.Type & SurfaceType.Additive) != 0, + Translucency = translucency, + MaterialState = RetailSetSurfaceMaterialState.Resolve( + spec.Type, + texturePresent: true, + textureHasPalette: spec.Paletted), SurfaceOpacity = 0.25f, - RetailSurfaceMask = spec.Mask, + RetailSurfaceMask = mask, CullMode = CullMode.Clockwise, IsCellShell = true, SourceSurfaceIndex = i, }); } mesh.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches; - Assert.NotNull(_meshManager.UploadMeshData(mesh)); + ObjectRenderData uploaded = Assert.IsType( + _meshManager.UploadMeshData(mesh)); + Assert.Equal( + batches.Select(static batch => batch.MaterialState), + uploaded.Batches.Select(static batch => batch.MaterialState)); Renderer = new EnvCellRenderer( Device, diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/GpuContractTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/GpuContractTests.cs index c42b9cd7..1082c086 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/GpuContractTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/GpuContractTests.cs @@ -80,12 +80,14 @@ public sealed class GpuContractTests // pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while // detail is now computed inside those same pipelines. Collapsing any of // these modes changes the corresponding subset's composition. - Assert.Equal(5, Enum.GetValues().Length); + Assert.Equal(7, Enum.GetValues().Length); Assert.Contains(GpuBlendMode.None, Enum.GetValues()); Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues()); Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues()); Assert.Contains(GpuBlendMode.Additive, Enum.GetValues()); + Assert.Contains(GpuBlendMode.RawAdditive, Enum.GetValues()); Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues()); + Assert.Contains(GpuBlendMode.InverseAdditive, Enum.GetValues()); } [Fact] diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs index 2a1f7c09..17da0f34 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanShaderManifestTests.cs @@ -45,7 +45,9 @@ public sealed class VulkanShaderManifestTests // single-pass building detail material now runs inside the base // fragment. Final alpha is authoredOpacity*liveFade*detail.a^2; // 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 // surface opacity and forwards it plus exact detail category/flags // flat to the fragment shader. The 16-byte ABI is unchanged. diff --git a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs index a19c0c79..8b760695 100644 --- a/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Gpu/Vk/VulkanViewportMappingTests.cs @@ -143,6 +143,12 @@ public sealed class VulkanViewportMappingTests Assert.Equal( (BlendFactor.SrcAlpha, BlendFactor.One), 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 // alpha would have silently changed how every inverse-alpha surface // composites. diff --git a/tests/AcDream.App.Tests/Rendering/RetailDetailTextureContractTests.cs b/tests/AcDream.App.Tests/Rendering/RetailDetailTextureContractTests.cs index e2ef0eac..fd391e69 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailDetailTextureContractTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailDetailTextureContractTests.cs @@ -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 Destination = new(0.17f, 0.37f, 0.71f, 0.29f); + public static TheoryData 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 PolicyRows() { var rows = new TheoryData(); @@ -65,27 +104,22 @@ public sealed class RetailDetailTextureContractTests Assert.Equal(["source-subset"], PhysicalDrawSequence(detailEnabled)); PipelineState state = ExpectedPipelineState( - consumer, expectedKind, paletted, expectedReference); - Assert.Equal(expectedReference, state.AlphaReference); - Assert.Equal( - expectedKind == TranslucencyKind.ClipMap - ? paletted ? 100f / 255f : 200f / 255f - : 0.05f, - state.AlphaReference); - Assert.True(state.AlphaTest); - Assert.Equal(expectedKind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap, - state.DepthWrite); - Assert.Equal(expectedKind switch + consumer, surfaceType, expectedKind, paletted, detailEnabled); + RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve( + surfaceType, + texturePresent: true, + textureHasPalette: paletted); + if (detailEnabled) { - TranslucencyKind.Opaque => GpuBlendMode.None, - TranslucencyKind.AlphaBlend => GpuBlendMode.StraightAlpha, - TranslucencyKind.Additive => GpuBlendMode.Additive, - TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha, - TranslucencyKind.ClipMap when consumer == "EnvCell" => - GpuBlendMode.PremultipliedAlpha, - TranslucencyKind.ClipMap => GpuBlendMode.None, - _ => throw new ArgumentOutOfRangeException(nameof(expectedKind)), - }, state.Blend); + Assert.Equal(resolved.AlphaTestReference, state.AlphaReference); + Assert.Equal(resolved.AlphaTestEnabled, state.AlphaTest); + Assert.Equal(resolved.FogEnabled, state.FogEnabled); + } + else + { + Assert.Equal(expectedReference, state.AlphaReference); + Assert.True(state.AlphaTest); + } Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled)); if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap) Assert.Equal("AP-240 immediate A2C", state.Placement); @@ -263,11 +297,38 @@ public sealed class RetailDetailTextureContractTests private static PipelineState ExpectedPipelineState( string consumer, + SurfaceType surfaceType, TranslucencyKind kind, 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 { TranslucencyKind.Opaque => GpuBlendMode.None, @@ -283,7 +344,10 @@ public sealed class RetailDetailTextureContractTests blend, kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap, AlphaTest: true, - reference, + kind == TranslucencyKind.ClipMap + ? paletted ? 100f / 255f : 200f / 255f + : 0.05f, + FogEnabled: true, consumer == "Building" && kind == TranslucencyKind.ClipMap ? "AP-240 immediate A2C" : "source FIFO position"); @@ -310,6 +374,7 @@ public sealed class RetailDetailTextureContractTests bool DepthWrite, bool AlphaTest, float AlphaReference, + bool FogEnabled, string Placement); private static Vector4 IndependentComposite( diff --git a/tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs index 9e5419ba..1782c9c4 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs @@ -32,7 +32,8 @@ public sealed class OrderPreservingSubmitterTests WalkDrawStage stage = WalkDrawStage.Terrain, TranslucencyKind translucency = TranslucencyKind.Opaque, CullMode cullMode = CullMode.CounterClockwise, - uint detailCategory = 0) => + uint detailCategory = 0, + RetailSetSurfaceMaterialState? materialState = null) => new( Key: new GroupKey( FirstIndex: (uint)index * 3, @@ -41,6 +42,7 @@ public sealed class OrderPreservingSubmitterTests TextureSlot: new GpuTextureSlot((uint)index), TextureLayer: 0, Translucency: translucency, + MaterialState: materialState ?? RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0, CullMode: cullMode), 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)); } - [Fact] - public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace() + [Theory] + [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 DrawScope draw = fx.BeginDraw(); @@ -293,7 +299,14 @@ public sealed class OrderPreservingSubmitterTests OrderedDrawStream stream = StreamOf( MakeCommand(0), 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)); PrepareAndDrawWhole(fx.Dispatcher, draw, stream); @@ -317,8 +330,90 @@ public sealed class OrderPreservingSubmitterTests .Select(call => call.PipelineName) .ToArray()); Assert.Equal(4, fx.Device.Calls.OfType().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(), + 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(), + 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] public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne() { @@ -609,7 +704,10 @@ public sealed class OrderPreservingSubmitterTests private readonly WbMeshAdapter _meshAdapter; 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(); FrameLifetime = new GpuDeviceFrameLifetime(Device); @@ -640,6 +738,15 @@ public sealed class OrderPreservingSubmitterTests new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16) : default, buildingDetailEnabled: () => detailEnabled); + if (atmospheric) + { + var source = new BindableAtmosphericSource(); + WbDrawDispatcher.DirectionalShadowReceiverPipelineState candidate = + Assert.IsType( + Dispatcher.PrepareDirectionalShadowReceiver(source, sampleCount: 1)); + Assert.Null(Dispatcher.SwapDirectionalShadowReceiver(candidate)); + } + Atmospheric = atmospheric; } public RecordingGpuDevice Device { get; } @@ -650,6 +757,8 @@ public sealed class OrderPreservingSubmitterTests public WbDrawDispatcher Dispatcher { get; } + public bool Atmospheric { get; } + /// Opens a frame and a backbuffer pass, publishes it on /// , then clears the recorded calls so a test only /// sees what its own SubmitOrderedStream call produced. @@ -659,7 +768,11 @@ public sealed class OrderPreservingSubmitterTests IGpuFrame frame = FrameLifetime.CurrentFrame!; IGpuPassEncoder pass = frame.BeginPass( 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); Device.Clear(); 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 { public PreparedAssetSourceStats Stats => default; diff --git a/tests/AcDream.App.Tests/Rendering/Walk/OrderedDrawStreamTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/OrderedDrawStreamTests.cs index b79158a3..2fed6f4f 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/OrderedDrawStreamTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/OrderedDrawStreamTests.cs @@ -28,6 +28,7 @@ public sealed class OrderedDrawStreamTests TextureSlot: new GpuTextureSlot((uint)index), TextureLayer: 0, Translucency: translucency, + MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0, CullMode: cullMode), Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3), diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs index 77add57c..68b1f646 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs @@ -105,11 +105,13 @@ public sealed class WalkStaticStreamPopulatorTests int indexCount, uint textureSlotIndex, uint textureLayer = 0, - CullMode cullMode = CullMode.CounterClockwise) => + CullMode cullMode = CullMode.CounterClockwise, + RetailSetSurfaceMaterialState? materialState = null) => new() { Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false }, Translucency = translucency, + MaterialState = materialState ?? RetailSetSurfaceMaterialState.Opaque, FirstIndex = firstIndex, BaseVertex = (uint)baseVertex, IndexCount = indexCount, @@ -316,14 +318,14 @@ public sealed class WalkStaticStreamPopulatorTests Assert.DoesNotContain( "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( "## 3. Documented approximation (AP)", StringComparison.Ordinal); int apSectionEnd = register.IndexOf( "## 4. Temporary stopgap (TS)", StringComparison.Ordinal); Assert.True(apSectionStart >= 0, "AP section heading must exist."); 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); Assert.True( @@ -337,8 +339,10 @@ public sealed class WalkStaticStreamPopulatorTests line.StartsWith("| AP-242 |", StringComparison.Ordinal)); Assert.Single(File.ReadLines(registerPath), static line => line.StartsWith("| AP-243 |", StringComparison.Ordinal)); + Assert.Single(File.ReadLines(registerPath), static line => + line.StartsWith("| AP-244 |", StringComparison.Ordinal)); Assert.Equal( - 160, + 161, File.ReadLines(registerPath).Count(static line => line.StartsWith("| AP-", StringComparison.Ordinal))); Assert.Contains("greater than 50 m", register, StringComparison.Ordinal); @@ -417,7 +421,12 @@ public sealed class WalkStaticStreamPopulatorTests InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh( MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1))); 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[] { @@ -448,6 +457,7 @@ public sealed class WalkStaticStreamPopulatorTests Assert.Equal(3u, translucent.Key.FirstIndex); Assert.Equal(6, translucent.Key.IndexCount); 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(2, selectionParts.Count); @@ -1095,7 +1105,8 @@ public sealed class WalkStaticStreamPopulatorTests using var fx = new DispatcherFixture(withAlphaQueue: true); 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); Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6); var batch = new WbDrawDispatcher.WalkClassifiedBatch( @@ -1221,13 +1232,35 @@ public sealed class WalkStaticStreamPopulatorTests } [Theory] - [InlineData(TranslucencyKind.AlphaBlend, "wb-mesh-alpha-1x")] - [InlineData(TranslucencyKind.Additive, "wb-mesh-additive-1x")] - [InlineData(TranslucencyKind.InvAlpha, "wb-mesh-inverse-1x")] - public void ImmediateBuildingDetail_RetainsOriginalFramebufferFamily( - TranslucencyKind kind, - string expectedPipeline) + [InlineData(SurfaceType.Alpha, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)] + [InlineData(SurfaceType.Alpha | SurfaceType.Additive, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 0f, false)] + [InlineData(SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)] + [InlineData(SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)] + [InlineData(SurfaceType.InvAlpha | SurfaceType.Additive, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 0f, false)] + [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( withAlphaQueue: true, detailAvailable: true, @@ -1235,7 +1268,8 @@ public sealed class WalkStaticStreamPopulatorTests using DrawScope draw = fx.BeginDraw(beginAlpha: true); WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch( detailCategory: 1u, - translucency: kind); + surfaceType: type, + paletted: paletted); fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity); @@ -1243,11 +1277,121 @@ public sealed class WalkStaticStreamPopulatorTests Assert.Single(fx.Device.Calls.OfType()); Assert.Contains(fx.Device.Calls.OfType(), call => call.PipelineName == expectedPipeline); + GpuPushConstants armed = fx.Device.Calls + .OfType() + .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()); + fx.AlphaQueue.EndFrame(); + Assert.Single(fx.Device.Calls.OfType()); + Assert.Contains(fx.Device.Calls.OfType(), + call => call.PipelineName == expectedPipeline); + GpuPushConstants constants = fx.Device.Calls + .OfType() + .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( uint detailCategory, - TranslucencyKind translucency = TranslucencyKind.AlphaBlend) => + SurfaceType surfaceType = SurfaceType.Alpha, + bool paletted = false) => new( new GroupKey( 0, @@ -1255,7 +1399,11 @@ public sealed class WalkStaticStreamPopulatorTests 3, new GpuTextureSlot(1), 0, - translucency, + TranslucencyKindExtensions.FromSurfaceType(surfaceType), + RetailSetSurfaceMaterialState.Resolve( + surfaceType, + texturePresent: true, + textureHasPalette: paletted), FoliageFlags: 0), Matrix4x4.Identity, ClipSlot: 0, @@ -1277,6 +1425,10 @@ public sealed class WalkStaticStreamPopulatorTests TextureSlot = new GpuTextureSlot(1), TextureLayer = 0, Translucency = TranslucencyKind.AlphaBlend, + MaterialState = RetailSetSurfaceMaterialState.Resolve( + SurfaceType.Alpha, + texturePresent: true, + textureHasPalette: false), }; group.Matrices.Add(Matrix4x4.Identity); group.SubmissionOrders.Add(0); @@ -1320,7 +1472,8 @@ public sealed class WalkStaticStreamPopulatorTests using var fx = new DispatcherFixture(withAlphaQueue: true); 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( key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10)); @@ -1354,7 +1507,8 @@ public sealed class WalkStaticStreamPopulatorTests var stream = new OrderedDrawStream(); 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, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0)); diff --git a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs index f95917ed..2f1465b4 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs @@ -28,6 +28,7 @@ using AcDream.App.Rendering.Gpu.Vk; using AcDream.App.Rendering.Wb; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; +using AcDream.Core.Meshing; using Microsoft.Extensions.Logging.Abstractions; using Xunit; using CullMode = DatReaderWriter.Enums.CullMode; @@ -186,7 +187,8 @@ public class EnvCellRendererTests "_mdiDrawRanges", BindingFlags.NonPublic | BindingFlags.Instance)!; var ranges = (List)rangesField.GetValue(renderer)!; 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(); for (int i = 0; i < instanceCount; i++) @@ -239,7 +241,7 @@ public class EnvCellRendererTests } [Fact] - public void DetailUsesOnlyTheOriginalFourShellPipelines() + public void DetailUsesOnlySetSurfaceShellPipelinesAndNoReplayPipeline() { using var device = new RecordingGpuDevice(); using var meshManager = CreateMeshManager(device); @@ -250,7 +252,7 @@ public class EnvCellRendererTests meshManager, new WbFrustum()); - Assert.Equal(4, device.CreatedPipelines.Count); + Assert.Equal(7, device.CreatedPipelines.Count); Assert.DoesNotContain(device.CreatedPipelines, pipeline => pipeline.Description.Name.Contains("detail", StringComparison.Ordinal)); } @@ -260,11 +262,14 @@ public class EnvCellRendererTests { var ranges = new List(); - EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3); - EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 3, commandCount: 4); + EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3, + RetailSetSurfaceMaterialState.Opaque); + EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 3, commandCount: 4, + RetailSetSurfaceMaterialState.Opaque); Assert.Equal( - [new EnvCellRenderer.MdiDrawRange(GroupIndex: 2, FirstCommand: 0, CommandCount: 7)], + [new EnvCellRenderer.MdiDrawRange(GroupIndex: 2, FirstCommand: 0, CommandCount: 7, + RetailSetSurfaceMaterialState.Opaque)], ranges); } @@ -273,17 +278,21 @@ public class EnvCellRendererTests { var ranges = new List(); - EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3); - EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 6, firstCommand: 3, commandCount: 2); - EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 5, commandCount: 1); - EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 9, commandCount: 2); + EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3, + RetailSetSurfaceMaterialState.Opaque); + EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 6, firstCommand: 3, 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( [ - new EnvCellRenderer.MdiDrawRange(2, 0, 3), - new EnvCellRenderer.MdiDrawRange(6, 3, 2), - new EnvCellRenderer.MdiDrawRange(2, 5, 1), - new EnvCellRenderer.MdiDrawRange(2, 9, 2), + new EnvCellRenderer.MdiDrawRange(2, 0, 3, RetailSetSurfaceMaterialState.Opaque), + new EnvCellRenderer.MdiDrawRange(6, 3, 2, RetailSetSurfaceMaterialState.Opaque), + new EnvCellRenderer.MdiDrawRange(2, 5, 1, RetailSetSurfaceMaterialState.Opaque), + new EnvCellRenderer.MdiDrawRange(2, 9, 2, RetailSetSurfaceMaterialState.Opaque), ], ranges); } @@ -293,7 +302,8 @@ public class EnvCellRendererTests { var ranges = new List(); - EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 0); + EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 0, + RetailSetSurfaceMaterialState.Opaque); Assert.Empty(ranges); } diff --git a/tests/AcDream.App.Tests/Rendering/Wb/FoliageWindClassificationTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/FoliageWindClassificationTests.cs index 25cfeadc..64ac8f9c 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/FoliageWindClassificationTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/FoliageWindClassificationTests.cs @@ -162,6 +162,7 @@ public sealed class FoliageWindClassificationTests TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(42), TextureLayer: 0, Translucency: TranslucencyKind.ClipMap, + MaterialState: RetailSetSurfaceMaterialState.Opaque, CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise, FoliageFlags: sceneryFlags); var landblockStaticKey = sceneryKey with { FoliageFlags = landblockStaticFlags }; diff --git a/tests/AcDream.App.Tests/Rendering/Wb/InstanceGroupClearTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/InstanceGroupClearTests.cs index 7925bdaf..2110d579 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/InstanceGroupClearTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/InstanceGroupClearTests.cs @@ -270,6 +270,7 @@ public class InstanceGroupClearTests TextureSlot: Slot(0xAA), TextureLayer: 0, Translucency: TranslucencyKind.Opaque, + MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0u); var cached = new CachedBatch( key, @@ -312,6 +313,7 @@ public class InstanceGroupClearTests TextureSlot: Slot(0x77), TextureLayer: 0, Translucency: TranslucencyKind.ClipMap, + MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0x2u, CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise); @@ -416,6 +418,7 @@ public class InstanceGroupClearTests TextureSlot: Slot(textureSlot), TextureLayer: 0, Translucency: TranslucencyKind.Opaque, + MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0u); private static WbDrawDispatcher.InstanceGroup MakeGroup( diff --git a/tests/AcDream.Content.Tests/MeshExtractorSolidFaceExtractionTests.cs b/tests/AcDream.Content.Tests/MeshExtractorSolidFaceExtractionTests.cs index 6600f621..62bc48b9 100644 --- a/tests/AcDream.Content.Tests/MeshExtractorSolidFaceExtractionTests.cs +++ b/tests/AcDream.Content.Tests/MeshExtractorSolidFaceExtractionTests.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using AcDream.Content; +using AcDream.Core.Meshing; using Chorizite.Core.Render.Enums; using DatReaderWriter; using DatReaderWriter.DBObjs; @@ -255,6 +256,60 @@ public sealed class MeshExtractorSolidFaceExtractionTests BitConverter.SingleToInt32Bits(batch.SurfaceOpacity)); } + public static TheoryData 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( + extractor.PrepareMeshData(GfxObjId, isSetup: false)); + ObjectMeshData cell = Assert.IsType( + 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); + } + /// /// OH2/S1 chunk-2 (2026-09-02): these two winding tests used to register /// an UNTEXTURED (Base1Solid) surface. Under the exact contract, an @@ -800,7 +855,8 @@ public sealed class MeshExtractorSolidFaceExtractionTests SurfaceType surfaceType, PixelFormat pixelFormat, byte[] sourceData, - float translucency = 0.0f) + float translucency = 0.0f, + uint paletteId = 0) { dats.RegisterRootGfxObj(GfxObjId, BuildQuadGfxObj(TexturedSurfaceId, noPos: false)); dats.Register(TexturedSurfaceId, new Surface @@ -818,6 +874,7 @@ public sealed class MeshExtractorSolidFaceExtractionTests Width = 4, Height = 4, Format = pixelFormat, + DefaultPaletteId = paletteId, SourceData = sourceData, }); } diff --git a/tests/AcDream.Content.Tests/ObjectMeshDataEquality.cs b/tests/AcDream.Content.Tests/ObjectMeshDataEquality.cs index 2acda9ae..070cd31b 100644 --- a/tests/AcDream.Content.Tests/ObjectMeshDataEquality.cs +++ b/tests/AcDream.Content.Tests/ObjectMeshDataEquality.cs @@ -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.Translucency == 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.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}"); diff --git a/tests/AcDream.Content.Tests/ObjectMeshDataSerializerTests.cs b/tests/AcDream.Content.Tests/ObjectMeshDataSerializerTests.cs index 863958d5..e538aac6 100644 --- a/tests/AcDream.Content.Tests/ObjectMeshDataSerializerTests.cs +++ b/tests/AcDream.Content.Tests/ObjectMeshDataSerializerTests.cs @@ -192,6 +192,10 @@ public class ObjectMeshDataSerializerTests { Indices = new List { 0, 1, 2 }, CullMode = CullMode.Clockwise, Translucency = TranslucencyKind.Opaque, + MaterialState = new RetailSetSurfaceMaterialState( + RetailSetSurfaceBlend.InverseAdditive, + RetailSetSurfaceAlphaTest.Paletted, + FogEnabled: false), IsTransparent = false, IsAdditive = false, HasWrappingUVs = false, @@ -290,6 +294,37 @@ public class ObjectMeshDataSerializerTests { 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 ----------------------------------------------------- [Fact] diff --git a/tests/AcDream.Content.Tests/PakFormatTests.cs b/tests/AcDream.Content.Tests/PakFormatTests.cs index fa663f4c..918aec2b 100644 --- a/tests/AcDream.Content.Tests/PakFormatTests.cs +++ b/tests/AcDream.Content.Tests/PakFormatTests.cs @@ -10,13 +10,14 @@ public class PakFormatTests { } /// - /// S5-c4: recipe 9 carries authored surface opacity in every prepared - /// texture batch. It remains a payload recipe change, not pak framing. + /// S5-c4 fix round 1: recipe 10 carries exact SetSurface state after + /// recipe 10's resolved SetSurface state after recipe 9's authored opacity. + /// It remains a payload recipe change. /// [Fact] public void FormatVersion_StaysAtTwo_AcrossTheS5C4RecipeBump() { Assert.Equal(2u, PakFormat.CurrentFormatVersion); - Assert.Equal(9u, PakFormat.CurrentBakeToolVersion); + Assert.Equal(10u, PakFormat.CurrentBakeToolVersion); } [Fact] diff --git a/tests/AcDream.Content.Tests/PreparedAssetSourceTests.cs b/tests/AcDream.Content.Tests/PreparedAssetSourceTests.cs index 02c99918..78eac707 100644 --- a/tests/AcDream.Content.Tests/PreparedAssetSourceTests.cs +++ b/tests/AcDream.Content.Tests/PreparedAssetSourceTests.cs @@ -136,7 +136,7 @@ public sealed class PreparedAssetSourceTests : IDisposable } /// - /// 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 /// alone, BEFORE any TextureBatchData payload field is ever /// deserialized -- so an older pak can never be silently misread as @@ -160,7 +160,7 @@ public sealed class PreparedAssetSourceTests : IDisposable fs.Position = 36; Span buf = stackalloc byte[4]; System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian( - buf, 8u); + buf, 9u); fs.Write(buf); } @@ -168,7 +168,7 @@ public sealed class PreparedAssetSourceTests : IDisposable () => new PakPreparedAssetSource(path, Identity)); Assert.Contains("does not match the installed DAT set", 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] diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs index 7e57619a..4ab64c84 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/EntityClassificationCacheTests.cs @@ -337,6 +337,7 @@ public class EntityClassificationCacheTests TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), TextureLayer: 0, Translucency: TranslucencyKind.Opaque, + MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0u); return new CachedBatch(key, new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), Matrix4x4.Identity); } diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherBucketingTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherBucketingTests.cs index 446b56d4..f6d9c447 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherBucketingTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherBucketingTests.cs @@ -426,6 +426,7 @@ public sealed class WbDrawDispatcherBucketingTests TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), TextureLayer: 0, Translucency: TranslucencyKind.Opaque, + MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0u); return new CachedBatch( key, diff --git a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherIndirectBuilderTests.cs b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherIndirectBuilderTests.cs index 5ae679d4..eace8c62 100644 --- a/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherIndirectBuilderTests.cs +++ b/tests/AcDream.Core.Tests/Rendering/Wb/WbDrawDispatcherIndirectBuilderTests.cs @@ -20,9 +20,9 @@ public sealed class WbDrawDispatcherIndirectBuilderTests // Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances) var groups = new List { - new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.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: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.5f), + 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, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.25f), + 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]; @@ -75,12 +75,15 @@ public sealed class WbDrawDispatcherIndirectBuilderTests { new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, + MaterialState: RetailSetSurfaceMaterialState.Opaque, CullMode: CullMode.Clockwise), new(IndexCount: 20, FirstIndex: 10, BaseVertex: 0, InstanceCount: 1, FirstInstance: 1, TextureIndex: 0x2, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, + MaterialState: RetailSetSurfaceMaterialState.Opaque, CullMode: CullMode.None), new(IndexCount: 30, FirstIndex: 30, BaseVertex: 0, InstanceCount: 1, FirstInstance: 2, TextureIndex: 0x3, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap, + MaterialState: RetailSetSurfaceMaterialState.Opaque, CullMode: CullMode.Landblock), }; var indirect = new DrawElementsIndirectCommand[4]; @@ -116,7 +119,7 @@ public sealed class WbDrawDispatcherIndirectBuilderTests // because the discard handles transparency, not blending. var groups = new List { - 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 batch = new WbDrawDispatcher.BatchDataPublic[4]; @@ -146,7 +149,8 @@ public sealed class WbDrawDispatcherIndirectBuilderTests FirstInstance: index, TextureIndex: (uint)index, TextureLayer: 0, - Translucency: kind)).ToList(); + Translucency: kind, + MaterialState: RetailSetSurfaceMaterialState.Opaque)).ToList(); var indirect = new DrawElementsIndirectCommand[kinds.Length]; var batches = new WbDrawDispatcher.BatchDataPublic[kinds.Length]; diff --git a/tests/AcDream.Launcher.Core.Tests/Installation/ContentMigrationCatalogTests.cs b/tests/AcDream.Launcher.Core.Tests/Installation/ContentMigrationCatalogTests.cs index ee8c347d..07bf4dc1 100644 --- a/tests/AcDream.Launcher.Core.Tests/Installation/ContentMigrationCatalogTests.cs +++ b/tests/AcDream.Launcher.Core.Tests/Installation/ContentMigrationCatalogTests.cs @@ -77,4 +77,28 @@ public sealed class ContentMigrationCatalogTests Assert.Contains("CellStruct", 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); + } }