fix(rendering): carry retail SetSurface state to detail draws
Resolve exact SetSurface blend, alpha-test, and fog state once during extraction and preserve it through recipe-10 prepared payloads, Wb/EnvCell command data, Vulkan pipelines, push constants, and both ordinary/atmospheric one-pass shaders. Preserve AP-240 Wb pure-Clip immediate opaque/A2C while EnvCell uses retail premultiplied Clip; detail-off routing remains unchanged. Correct AP-232 and register the remaining detail-off state divergence. Mutation first failures (all restored): - raw Add->SRCALPHA/ONE: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState line 1278, expected wb-mesh-raw-additive-1x. - inverse-add->alpha-add: same test line 1278, expected wb-mesh-inverse-additive-1x. - remove Env inverse: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 132, expected envcell-inverse. - raw IsAdditive precedence: same test line 132, Translucent|Clip|Additive expected envcell-alpha. - Wb paletted ParamB=0: OrderPreservingSubmitterTests line 333, expected 0.392156869. - Wb DDS ParamB=0.05: same test line 333, expected 0.784313738. - disable Alpha+Clip test: WalkStaticStreamPopulatorTests line 1286, expected 0.784313738. - always fog raw Add: same test line 1287, expected no-fog true. - disable fog non-Add: same test line 1287, expected no-fog false. - X=a*qA: RetailDetailTextureContractTests line 261, shared squared-alpha substring absent. - X includes base alpha: same test line 262, forbidden baseTexel.a present. - CLIP uses 0.05: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 367. - second detail draw: EnvCell detail-on line 131, collection contained 2 draws. - straight-alpha substitute: Wb immediate line 1278, expected wb-mesh-additive-1x. - omit ordered detail arm: OrderPreservingSubmitterTests line 318, expected (77,3.5), got (0,0). - omit atmospheric combine: RetailDetailTextureContractTests line 260, shared include absent. - drop serialized opacity: ObjectMeshDataSerializerTests line 292, expected opacity bits, got 1.0. - stale detail arm: atmospheric adjacency line 402, expected slot 0, got 77. - per-frame surface map: EnvCell warmed allocation line 285, expected 0 B, got 204800 B.
This commit is contained in:
parent
75664805f8
commit
15ed57a1e7
44 changed files with 1154 additions and 187 deletions
|
|
@ -259,8 +259,22 @@ heading/velocity/parent state, arms no teleport hook, and still acknowledges
|
||||||
immediately. The long AP-148 row below is retained only as historical filing
|
immediately. The long AP-148 row below is retained only as historical filing
|
||||||
research and is no longer active.
|
research and is no longer active.
|
||||||
|
|
||||||
|
**S5-c4 fix-round-1 AP-232 correction (2026-09-05):** AP-244 below raises
|
||||||
|
the active total to **161**; the historical heading's 160 is superseded.
|
||||||
|
AP-232's retirement remains valid for the old two-draw/detail-weight
|
||||||
|
deviation, with this corrected scope: recipe 10 carries extraction-resolved
|
||||||
|
SetSurface blend, alpha-test/reference, and fog state through both real command
|
||||||
|
owners. Detail-active Wb and EnvCell select raw Additive, Alpha+Additive,
|
||||||
|
inverse, inverse-additive, combined/pure ClipMap, late Translucent override,
|
||||||
|
final-X reference, and raw-Additive fog-disable state exactly. Wb pure Clip
|
||||||
|
retains AP-240's immediate opaque/A2C pipeline while gaining final-X ParamB;
|
||||||
|
only EnvCell pure Clip uses `ONE/INVSRCALPHA`. The retired row's recipe-9-only
|
||||||
|
and `None` residual wording is superseded by this correction and AP-244;
|
||||||
|
AP-238/AP-239/AP-240 remain separate queue/token/raw-mask/placement residuals.
|
||||||
|
|
||||||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
|
| AP-244 | **Filed 2026-09-05 at Campaign OVERHAUL S5-c4 fix round 1.** The new exact resolved SetSurface state is deliberately selected only when a Wb/EnvCell command has active building detail, preserving §23's bounded detail-off behavior. With detail off, Wb still collapses raw Additive and InvAlpha+Additive through `TranslucencyKind.Additive` to `SRCALPHA,ONE`; EnvCell additionally selects raw `IsAdditive` before the late `Translucent\|ClipMap` override and has no detail-off inverse pipeline. Both shader families retain the existing non-detail 0.05 discard rather than every combined ClipMap's palette/DDS reference, and fixed-function fog remains enabled for raw-Additive detail-off rows. AP-240 separately covers Wb pure-Clip placement/A2C. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`PipelineForBlend`, detail gates); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`ResolveBatchGroupIndex`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (detail-active exact selection versus retained range buckets); `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag` | Fix round 1 was expressly bounded to detail-active one-pass material truth. The immutable state is already present end-to-end, so a later dedicated detail-off correction can select it without DAT lookup or new ownership, but must re-gate FIFO/routing/order and visual behavior together. | Detail-off raw Additive/inverse-additive brightness, inverse EnvCell compositing, combined ClipMap cutout threshold, late Translucent override, and raw-Additive fog can differ from retail. | `D3DPolyRender::SetSurface @0x0059C4D0` lines 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<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001` → `ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` |
|
| AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001` → `ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` |
|
||||||
| AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` |
|
| AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` |
|
||||||
| ~~AP-232~~ | **RETIRED 2026-09-04 at Campaign OVERHAUL S5-c4.** The acdream-only base-plus-detail replay is deleted. `TextureBatchData.SurfaceOpacity` carries exact `1-Surface.Translucency` from `MeshExtractor` through recipe-9 prepared serialization, `ObjectRenderBatch`, and the unchanged 16-byte GPU batch ABI. `mesh_modern` and `mesh_atmospheric` share `retail_detail_material.glsl` and emit one pre-fog fragment per detail-active subset: `w=a*qA`, `C=q*w+(B*D)*(1-w)`, `X=a*qA*qA`, where `a=authoredOpacity*liveFade`; base texture alpha is excluded. The final `X` drives pure CLIP at 100/255 or 200/255 with equality surviving. Classic grouped, walk-ordered (#471), immediate alpha, delayed alpha, and EnvCell paths keep their original draw position and pipeline family; the separate RetailDetail pipelines, replay helpers, shaders, SPIR-V, and manifest entry are gone. | `src/AcDream.Content/MeshExtractor.cs`; `src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs`; `src/AcDream.App/Rendering/Shaders/retail_detail_material.glsl`; `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs`; `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` | Exact retail single-pass stage arithmetic, authored-alpha provenance, framebuffer-family/CLIP CPU fixtures, shader ABI pins, deterministic recipe-9 round trips and rejection of recipe 8, one-draw command transcripts, shader/SPIR-V validation, and 0-B warmed-path proof. | None for the former weight/draw-count deviation. AP-238/AP-239/AP-240 remain separate queue/token/raw-mask placement residuals and are not changed by this retirement. | `D3DPolyRender::SetSurface @0x0059C4D0`; `RenderMeshSubset @0x0059CA10`; `DrawMesh @0x0059D4A0`; VM2 cdb note `docs/research/2026-08-22-vm2-retail-detail-path-cdb.md`; Campaign OVERHAUL S5-c4 §22 |
|
| ~~AP-232~~ | **RETIRED 2026-09-04 at Campaign OVERHAUL S5-c4.** The acdream-only base-plus-detail replay is deleted. `TextureBatchData.SurfaceOpacity` carries exact `1-Surface.Translucency` from `MeshExtractor` through recipe-9 prepared serialization, `ObjectRenderBatch`, and the unchanged 16-byte GPU batch ABI. `mesh_modern` and `mesh_atmospheric` share `retail_detail_material.glsl` and emit one pre-fog fragment per detail-active subset: `w=a*qA`, `C=q*w+(B*D)*(1-w)`, `X=a*qA*qA`, where `a=authoredOpacity*liveFade`; base texture alpha is excluded. The final `X` drives pure CLIP at 100/255 or 200/255 with equality surviving. Classic grouped, walk-ordered (#471), immediate alpha, delayed alpha, and EnvCell paths keep their original draw position and pipeline family; the separate RetailDetail pipelines, replay helpers, shaders, SPIR-V, and manifest entry are gone. | `src/AcDream.Content/MeshExtractor.cs`; `src/AcDream.Content/Pak/ObjectMeshDataSerializer.cs`; `src/AcDream.App/Rendering/Shaders/retail_detail_material.glsl`; `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs`; `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` | Exact retail single-pass stage arithmetic, authored-alpha provenance, framebuffer-family/CLIP CPU fixtures, shader ABI pins, deterministic recipe-9 round trips and rejection of recipe 8, one-draw command transcripts, shader/SPIR-V validation, and 0-B warmed-path proof. | None for the former weight/draw-count deviation. AP-238/AP-239/AP-240 remain separate queue/token/raw-mask placement residuals and are not changed by this retirement. | `D3DPolyRender::SetSurface @0x0059C4D0`; `RenderMeshSubset @0x0059CA10`; `DrawMesh @0x0059D4A0`; VM2 cdb note `docs/research/2026-08-22-vm2-retail-detail-path-cdb.md`; Campaign OVERHAUL S5-c4 §22 |
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,12 @@ internal enum GpuBlendMode
|
||||||
/// <summary>Additive: <c>SrcAlpha, One</c>.</summary>
|
/// <summary>Additive: <c>SrcAlpha, One</c>.</summary>
|
||||||
Additive,
|
Additive,
|
||||||
|
|
||||||
|
/// <summary>Retail raw Additive: <c>One, One</c>.</summary>
|
||||||
|
RawAdditive,
|
||||||
|
|
||||||
|
/// <summary>Retail inverse-additive: <c>OneMinusSrcAlpha, One</c>.</summary>
|
||||||
|
InverseAdditive,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retail's inverse-alpha translucency: <c>OneMinusSrcAlpha, SrcAlpha</c>.
|
/// Retail's inverse-alpha translucency: <c>OneMinusSrcAlpha, SrcAlpha</c>.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,8 @@ internal static class VulkanViewportMapping
|
||||||
GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
||||||
GpuBlendMode.PremultipliedAlpha => (BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
|
GpuBlendMode.PremultipliedAlpha => (BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
|
||||||
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
||||||
|
GpuBlendMode.RawAdditive => (BlendFactor.One, BlendFactor.One),
|
||||||
|
GpuBlendMode.InverseAdditive => (BlendFactor.OneMinusSrcAlpha, BlendFactor.One),
|
||||||
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
||||||
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||||
GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero),
|
GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero),
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ namespace AcDream.App.Rendering;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class RetailDetailTextureContract
|
internal static class RetailDetailTextureContract
|
||||||
{
|
{
|
||||||
|
internal const int NoFogRenderPassFlag = 0x200;
|
||||||
|
|
||||||
internal static bool ShouldRender(
|
internal static bool ShouldRender(
|
||||||
bool settingEnabled,
|
bool settingEnabled,
|
||||||
TerrainAtlas.RetailDetailTextureBinding binding) =>
|
TerrainAtlas.RetailDetailTextureBinding binding) =>
|
||||||
|
|
|
||||||
|
|
@ -128,9 +128,12 @@ void main() {
|
||||||
alpha = color.a * vOpacityMultiplier;
|
alpha = color.a * vOpacityMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)
|
if (detailActive
|
||||||
|
? isRetailClipReference(uParamB) && alpha < uParamB
|
||||||
|
: color.a < alphaCutoff)
|
||||||
discard;
|
discard;
|
||||||
|
|
||||||
|
if (!detailActive || (uRenderPass & 0x200) == 0)
|
||||||
rgb = applyFog(rgb, vWorldPos);
|
rgb = applyFog(rgb, vWorldPos);
|
||||||
FragColor = vec4(rgb, alpha);
|
FragColor = vec4(rgb, alpha);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ in flat uint vDetailCategory;
|
||||||
// discard alpha >= 0.95 (already drawn opaque) and
|
// discard alpha >= 0.95 (already drawn opaque) and
|
||||||
// alpha < 0.05 (skip empty fragments — large
|
// alpha < 0.05 (skip empty fragments — large
|
||||||
// transparent overdraw cost otherwise)
|
// transparent overdraw cost otherwise)
|
||||||
|
// bit 0x200 = detail-active raw Additive; bypass fixed-function fog
|
||||||
uniform int uRenderPass;
|
uniform int uRenderPass;
|
||||||
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
||||||
// S4-c2 F3: only retail's exact pure-ClipMap alpha-test references are
|
// S4-c2 F3: only retail's exact pure-ClipMap alpha-test references are
|
||||||
|
|
@ -120,9 +121,12 @@ void main() {
|
||||||
|
|
||||||
// '<' spells D3DCMP_GREATEREQUAL: equality passes. Detail-active CLIP
|
// '<' spells D3DCMP_GREATEREQUAL: equality passes. Detail-active CLIP
|
||||||
// tests retail's final stage-1 alpha; detail-off keeps base-alpha logic.
|
// tests retail's final stage-1 alpha; detail-off keeps base-alpha logic.
|
||||||
if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)
|
if (detailActive
|
||||||
|
? isRetailClipReference(uParamB) && alpha < uParamB
|
||||||
|
: color.a < alphaCutoff)
|
||||||
discard;
|
discard;
|
||||||
|
|
||||||
|
if (!detailActive || (uRenderPass & 0x200) == 0)
|
||||||
rgb = applyFog(rgb, vWorldPos);
|
rgb = applyFog(rgb, vWorldPos);
|
||||||
FragColor = vec4(rgb, alpha);
|
FragColor = vec4(rgb, alpha);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -220,7 +220,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "5eb058e92ab27f1f6c245401e05f78a278fe00696b5c77f36ae33f8428f83bf3",
|
"sourceSha256": "c95e372c557a8a4b252ecf824ad049fe9611b0a1e49b0e6d18938d373d9c6794",
|
||||||
"compiled": true
|
"compiled": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -236,7 +236,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "396c382af73505bc2cac7a2db5878a63166730e47096a94630c35ad8376fe565",
|
"sourceSha256": "e145f32cbd50edcbc5e3196a49f0e2b05548bc0199ce13139e01c443794473b0",
|
||||||
"compiled": true
|
"compiled": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using AcDream.App.Rendering.Gpu;
|
using AcDream.App.Rendering.Gpu;
|
||||||
using AcDream.Core.Lighting;
|
using AcDream.Core.Lighting;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
@ -28,6 +29,9 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
private IGpuPipeline? _alphaPipeline;
|
private IGpuPipeline? _alphaPipeline;
|
||||||
private IGpuPipeline? _clipPipeline;
|
private IGpuPipeline? _clipPipeline;
|
||||||
private IGpuPipeline? _additivePipeline;
|
private IGpuPipeline? _additivePipeline;
|
||||||
|
private IGpuPipeline? _rawAdditivePipeline;
|
||||||
|
private IGpuPipeline? _inversePipeline;
|
||||||
|
private IGpuPipeline? _inverseAdditivePipeline;
|
||||||
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
||||||
private readonly Func<bool> _buildingDetailEnabled;
|
private readonly Func<bool> _buildingDetailEnabled;
|
||||||
|
|
||||||
|
|
@ -48,7 +52,7 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
||||||
/// four pipelines ARE this renderer's program, so there is no second step
|
/// SetSurface pipeline family IS this renderer's program, so there is no second step
|
||||||
/// and no <c>Shader</c> to hand in.
|
/// and no <c>Shader</c> to hand in.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal EnvCellRenderer(
|
internal EnvCellRenderer(
|
||||||
|
|
@ -80,6 +84,12 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
scope.SampleCount);
|
scope.SampleCount);
|
||||||
_additivePipeline = CreateShellPipeline(
|
_additivePipeline = CreateShellPipeline(
|
||||||
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
||||||
|
_rawAdditivePipeline = CreateShellPipeline(
|
||||||
|
device, "envcell-raw-additive", GpuBlendMode.RawAdditive, depthWrite: false, scope.SampleCount);
|
||||||
|
_inversePipeline = CreateShellPipeline(
|
||||||
|
device, "envcell-inverse", GpuBlendMode.InverseAlpha, depthWrite: false, scope.SampleCount);
|
||||||
|
_inverseAdditivePipeline = CreateShellPipeline(
|
||||||
|
device, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, depthWrite: false, scope.SampleCount);
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,7 +282,9 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
: isAdditive
|
: isAdditive
|
||||||
? _additivePipeline!
|
? _additivePipeline!
|
||||||
: _alphaPipeline!;
|
: _alphaPipeline!;
|
||||||
if (renderPass == WbRenderPass.Transparent)
|
if (detailEnabled)
|
||||||
|
rangeBasePipeline = PipelineForMaterial(drawRange.MaterialState);
|
||||||
|
if (renderPass == WbRenderPass.Transparent || detailEnabled)
|
||||||
{
|
{
|
||||||
// Blend state is the pipeline's; switching variants mid-pass has
|
// Blend state is the pipeline's; switching variants mid-pass has
|
||||||
// to re-establish the mesh, which is vertex-array state.
|
// to re-establish the mesh, which is vertex-array state.
|
||||||
|
|
@ -289,8 +301,12 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
pushConstants.RenderPass = isAdditive
|
pushConstants.RenderPass = isAdditive
|
||||||
? (int)renderPass | 0x100
|
? (int)renderPass | 0x100
|
||||||
: (int)renderPass;
|
: (int)renderPass;
|
||||||
|
if (detailEnabled && !drawRange.MaterialState.FogEnabled)
|
||||||
|
pushConstants.RenderPass |= RetailDetailTextureContract.NoFogRenderPassFlag;
|
||||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||||
pushConstants.ParamB = isClip
|
pushConstants.ParamB = detailEnabled
|
||||||
|
? drawRange.MaterialState.AlphaTestReference
|
||||||
|
: isClip
|
||||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||||
: 0f;
|
: 0f;
|
||||||
pushConstants.TextureIndexA = detailEnabled
|
pushConstants.TextureIndexA = detailEnabled
|
||||||
|
|
@ -311,9 +327,23 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
pushConstants.TextureIndexA = 0;
|
pushConstants.TextureIndexA = 0;
|
||||||
pushConstants.ParamA = 0f;
|
pushConstants.ParamA = 0f;
|
||||||
pushConstants.ParamB = 0f;
|
pushConstants.ParamB = 0f;
|
||||||
|
pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag;
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IGpuPipeline PipelineForMaterial(RetailSetSurfaceMaterialState material) =>
|
||||||
|
material.Blend switch
|
||||||
|
{
|
||||||
|
RetailSetSurfaceBlend.Opaque => _opaquePipeline!,
|
||||||
|
RetailSetSurfaceBlend.StraightAlpha => _alphaPipeline!,
|
||||||
|
RetailSetSurfaceBlend.AlphaAdditive => _additivePipeline!,
|
||||||
|
RetailSetSurfaceBlend.Additive => _rawAdditivePipeline!,
|
||||||
|
RetailSetSurfaceBlend.InverseAlpha => _inversePipeline!,
|
||||||
|
RetailSetSurfaceBlend.InverseAdditive => _inverseAdditivePipeline!,
|
||||||
|
RetailSetSurfaceBlend.Clip => _clipPipeline!,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(material), material, "Unknown SetSurface blend."),
|
||||||
|
};
|
||||||
|
|
||||||
private void BindPipelineWithMesh(
|
private void BindPipelineWithMesh(
|
||||||
IGpuPassEncoder encoder,
|
IGpuPassEncoder encoder,
|
||||||
IGpuPipeline pipeline,
|
IGpuPipeline pipeline,
|
||||||
|
|
@ -429,5 +459,11 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
_clipPipeline = null;
|
_clipPipeline = null;
|
||||||
_additivePipeline?.Dispose();
|
_additivePipeline?.Dispose();
|
||||||
_additivePipeline = null;
|
_additivePipeline = null;
|
||||||
|
_rawAdditivePipeline?.Dispose();
|
||||||
|
_rawAdditivePipeline = null;
|
||||||
|
_inversePipeline?.Dispose();
|
||||||
|
_inversePipeline = null;
|
||||||
|
_inverseAdditivePipeline?.Dispose();
|
||||||
|
_inverseAdditivePipeline = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
@ -162,7 +163,11 @@ public sealed partial class EnvCellRenderer :
|
||||||
private readonly List<MdiDrawRange> _mdiDrawRanges = new();
|
private readonly List<MdiDrawRange> _mdiDrawRanges = new();
|
||||||
|
|
||||||
private readonly record struct DrawCallRange(int First, int Count);
|
private readonly record struct DrawCallRange(int First, int Count);
|
||||||
internal readonly record struct MdiDrawRange(int GroupIndex, int FirstCommand, int CommandCount);
|
internal readonly record struct MdiDrawRange(
|
||||||
|
int GroupIndex,
|
||||||
|
int FirstCommand,
|
||||||
|
int CommandCount,
|
||||||
|
RetailSetSurfaceMaterialState MaterialState);
|
||||||
// Unfiltered rendering is retained for diagnostic callers only. Build its
|
// Unfiltered rendering is retained for diagnostic callers only. Build its
|
||||||
// global grouping lazily instead of duplicating every prepared gameplay
|
// global grouping lazily instead of duplicating every prepared gameplay
|
||||||
// instance in both a per-cell and global tree each frame.
|
// instance in both a per-cell and global tree each frame.
|
||||||
|
|
@ -883,7 +888,7 @@ public sealed partial class EnvCellRenderer :
|
||||||
EnvCellTransparentRoute transparentRoute,
|
EnvCellTransparentRoute transparentRoute,
|
||||||
bool detailSurfaceActive)
|
bool detailSurfaceActive)
|
||||||
{
|
{
|
||||||
// WB EnvCellRenderManager.cs:400: the RHI arm's four pipelines are built
|
// WB EnvCellRenderManager.cs:400: the RHI arm's SetSurface pipelines are built
|
||||||
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
||||||
// answers whether this renderer is ready to draw.
|
// answers whether this renderer is ready to draw.
|
||||||
if (!_initialized) return;
|
if (!_initialized) return;
|
||||||
|
|
@ -1304,7 +1309,6 @@ public sealed partial class EnvCellRenderer :
|
||||||
int groupIndex = _activeCullGroups[activeIndex];
|
int groupIndex = _activeCullGroups[activeIndex];
|
||||||
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
||||||
_batchesByCullGroup[groupIndex];
|
_batchesByCullGroup[groupIndex];
|
||||||
int firstCommand = cmdIndex;
|
|
||||||
foreach (var item in group)
|
foreach (var item in group)
|
||||||
{
|
{
|
||||||
_modernBatches[cmdIndex] = new ModernBatchData
|
_modernBatches[cmdIndex] = new ModernBatchData
|
||||||
|
|
@ -1330,22 +1334,14 @@ public sealed partial class EnvCellRenderer :
|
||||||
BaseVertex = (int)item.batch.BaseVertex,
|
BaseVertex = (int)item.batch.BaseVertex,
|
||||||
BaseInstance = (uint)item.instanceOffset,
|
BaseInstance = (uint)item.instanceOffset,
|
||||||
};
|
};
|
||||||
cmdIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
int commandCount = cmdIndex - firstCommand;
|
|
||||||
if (commandCount == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Adjacent cells frequently resolve to the same cull/blend
|
|
||||||
// state. Their commands are already contiguous and remain in
|
|
||||||
// strict cell order, so one MDI call can cover the complete run
|
|
||||||
// without changing alpha compositing or gl_DrawID indexing.
|
|
||||||
AppendMdiDrawRange(
|
AppendMdiDrawRange(
|
||||||
_mdiDrawRanges,
|
_mdiDrawRanges,
|
||||||
groupIndex,
|
groupIndex,
|
||||||
firstCommand,
|
cmdIndex,
|
||||||
commandCount);
|
1,
|
||||||
|
item.batch.MaterialState);
|
||||||
|
cmdIndex++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1400,7 +1396,8 @@ public sealed partial class EnvCellRenderer :
|
||||||
List<MdiDrawRange> ranges,
|
List<MdiDrawRange> ranges,
|
||||||
int groupIndex,
|
int groupIndex,
|
||||||
int firstCommand,
|
int firstCommand,
|
||||||
int commandCount)
|
int commandCount,
|
||||||
|
RetailSetSurfaceMaterialState materialState)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(ranges);
|
ArgumentNullException.ThrowIfNull(ranges);
|
||||||
if (commandCount <= 0)
|
if (commandCount <= 0)
|
||||||
|
|
@ -1410,6 +1407,7 @@ public sealed partial class EnvCellRenderer :
|
||||||
{
|
{
|
||||||
MdiDrawRange previous = ranges[^1];
|
MdiDrawRange previous = ranges[^1];
|
||||||
if (previous.GroupIndex == groupIndex
|
if (previous.GroupIndex == groupIndex
|
||||||
|
&& previous.MaterialState == materialState
|
||||||
&& previous.FirstCommand + previous.CommandCount == firstCommand)
|
&& previous.FirstCommand + previous.CommandCount == firstCommand)
|
||||||
{
|
{
|
||||||
ranges[^1] = previous with
|
ranges[^1] = previous with
|
||||||
|
|
@ -1420,7 +1418,7 @@ public sealed partial class EnvCellRenderer :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ranges.Add(new MdiDrawRange(groupIndex, firstCommand, commandCount));
|
ranges.Add(new MdiDrawRange(groupIndex, firstCommand, commandCount, materialState));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ internal readonly record struct GroupKey(
|
||||||
GpuTextureSlot TextureSlot,
|
GpuTextureSlot TextureSlot,
|
||||||
uint TextureLayer,
|
uint TextureLayer,
|
||||||
TranslucencyKind Translucency,
|
TranslucencyKind Translucency,
|
||||||
|
RetailSetSurfaceMaterialState MaterialState,
|
||||||
uint FoliageFlags,
|
uint FoliageFlags,
|
||||||
float SurfaceOpacity = 1f,
|
float SurfaceOpacity = 1f,
|
||||||
CullMode CullMode = CullMode.CounterClockwise);
|
CullMode CullMode = CullMode.CounterClockwise);
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ namespace AcDream.App.Rendering.Wb
|
||||||
public TextureKey Key { get; set; }
|
public TextureKey Key { get; set; }
|
||||||
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
|
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
|
||||||
public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; }
|
public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; }
|
||||||
|
public AcDream.Core.Meshing.RetailSetSurfaceMaterialState MaterialState { get; set; }
|
||||||
|
= AcDream.Core.Meshing.RetailSetSurfaceMaterialState.Opaque;
|
||||||
public bool IsTransparent { get; set; }
|
public bool IsTransparent { get; set; }
|
||||||
public bool IsAdditive { get; set; }
|
public bool IsAdditive { get; set; }
|
||||||
public bool HasWrappingUVs { get; set; }
|
public bool HasWrappingUVs { get; set; }
|
||||||
|
|
@ -2262,6 +2264,7 @@ namespace AcDream.App.Rendering.Wb
|
||||||
TextureFormat = format.Format,
|
TextureFormat = format.Format,
|
||||||
RetailSurfaceMask = batch.RetailSurfaceMask,
|
RetailSurfaceMask = batch.RetailSurfaceMask,
|
||||||
Translucency = batch.Translucency,
|
Translucency = batch.Translucency,
|
||||||
|
MaterialState = batch.MaterialState,
|
||||||
IsTransparent = batch.IsTransparent,
|
IsTransparent = batch.IsTransparent,
|
||||||
IsAdditive = batch.IsAdditive,
|
IsAdditive = batch.IsAdditive,
|
||||||
HasWrappingUVs = batch.HasWrappingUVs,
|
HasWrappingUVs = batch.HasWrappingUVs,
|
||||||
|
|
|
||||||
|
|
@ -488,7 +488,14 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
if (global is null)
|
if (global is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
MeshPipelineSet pipelines = PipelinesFor(encoder);
|
IGpuFrame frame = _orderedFrame
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"DrawOrderedRange has no frame to bind clip-region/"
|
||||||
|
+ "scene-lighting sections against — PrepareOrderedStream must run first.");
|
||||||
|
MeshPipelineSet pipelines = PipelinesFor(
|
||||||
|
encoder,
|
||||||
|
frame,
|
||||||
|
out DirectionalShadowFrameBinding shadowBinding);
|
||||||
var pushConstants = new GpuPushConstants
|
var pushConstants = new GpuPushConstants
|
||||||
{
|
{
|
||||||
ViewProjection = _orderedViewProjection,
|
ViewProjection = _orderedViewProjection,
|
||||||
|
|
@ -503,11 +510,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
IGpuFrame frame = _orderedFrame
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
"DrawOrderedRange has no frame to bind clip-region/"
|
|
||||||
+ "scene-lighting sections against — PrepareOrderedStream must run first.");
|
|
||||||
|
|
||||||
// Bind the SECTIONS on EVERY range call — never latch them
|
// Bind the SECTIONS on EVERY range call — never latch them
|
||||||
// across calls. Between ordered ranges the walk's leaf draws run
|
// across calls. Between ordered ranges the walk's leaf draws run
|
||||||
// (terrain, cell shells, sky, punch fans) and RetailAlphaQueue
|
// (terrain, cell shells, sky, punch fans) and RetailAlphaQueue
|
||||||
|
|
@ -521,6 +523,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
// DrawPreparedAlphaBatchRhi does for the same reason.
|
// DrawPreparedAlphaBatchRhi does for the same reason.
|
||||||
BindPipelineWithMesh(encoder, pipelines.Opaque, global);
|
BindPipelineWithMesh(encoder, pipelines.Opaque, global);
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
|
BindDirectionalShadowReceiver(encoder, in shadowBinding);
|
||||||
BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances);
|
BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances);
|
||||||
BindSection(encoder, GpuBindingModel.StorageBatches, _orderedBatches);
|
BindSection(encoder, GpuBindingModel.StorageBatches, _orderedBatches);
|
||||||
BindSection(encoder, GpuBindingModel.StorageClipSlots, _orderedClipSlots);
|
BindSection(encoder, GpuBindingModel.StorageClipSlots, _orderedClipSlots);
|
||||||
|
|
@ -567,11 +570,16 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
ValidateMergeRun(_orderedStream, run);
|
ValidateMergeRun(_orderedStream, run);
|
||||||
|
|
||||||
PipelineBucket bucket = BucketFor(_orderedStream.Keys[run.FirstCommand].Translucency);
|
PipelineBucket bucket = BucketFor(_orderedStream.Keys[run.FirstCommand].Translucency);
|
||||||
IGpuPipeline pipeline = PipelineForBucket(pipelines, bucket);
|
GroupKey key = _orderedStream.Keys[run.FirstCommand];
|
||||||
|
bool hasDetail = detailEnabled
|
||||||
|
&& _orderedStream.DetailCategories[run.FirstCommand] != 0u;
|
||||||
|
IGpuPipeline bucketPipeline = PipelineForBucket(pipelines, bucket);
|
||||||
|
IGpuPipeline pipeline = hasDetail
|
||||||
|
? PipelineForMaterial(pipelines, key.MaterialState, bucketPipeline)
|
||||||
|
: bucketPipeline;
|
||||||
pushConstants.RenderPass = bucket == PipelineBucket.Opaque ? 0 : 1;
|
pushConstants.RenderPass = bucket == PipelineBucket.Opaque ? 0 : 1;
|
||||||
if (detailEnabled
|
if (hasDetail)
|
||||||
&& _orderedStream.DetailCategories[run.FirstCommand] != 0u)
|
ArmBuildingDetail(ref pushConstants, key.MaterialState);
|
||||||
ArmBuildingDetail(ref pushConstants);
|
|
||||||
else
|
else
|
||||||
ClearDetailPushConstants(ref pushConstants);
|
ClearDetailPushConstants(ref pushConstants);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,9 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
IGpuPipeline OpaqueAlphaToCoverage,
|
IGpuPipeline OpaqueAlphaToCoverage,
|
||||||
IGpuPipeline AlphaBlend,
|
IGpuPipeline AlphaBlend,
|
||||||
IGpuPipeline AlphaAdditive,
|
IGpuPipeline AlphaAdditive,
|
||||||
IGpuPipeline AlphaInverse);
|
IGpuPipeline RawAdditive,
|
||||||
|
IGpuPipeline AlphaInverse,
|
||||||
|
IGpuPipeline InverseAdditive);
|
||||||
|
|
||||||
private MeshPipelineSet? _backbufferPipelines;
|
private MeshPipelineSet? _backbufferPipelines;
|
||||||
private MeshPipelineSet? _offscreenPipelines;
|
private MeshPipelineSet? _offscreenPipelines;
|
||||||
|
|
@ -227,7 +229,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
bool usesRenderPackShaderAbi = false)
|
bool usesRenderPackShaderAbi = false)
|
||||||
{
|
{
|
||||||
string suffix = samples > 1 ? string.Empty : "-1x";
|
string suffix = samples > 1 ? string.Empty : "-1x";
|
||||||
var created = new List<IGpuPipeline>(5);
|
var created = new List<IGpuPipeline>(7);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new MeshPipelineSet(
|
return new MeshPipelineSet(
|
||||||
|
|
@ -252,10 +254,20 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
shaders: baseShaders,
|
shaders: baseShaders,
|
||||||
shaderName: baseShaderName,
|
shaderName: baseShaderName,
|
||||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||||
|
Track(CreateMeshPipeline(
|
||||||
|
device, $"{namePrefix}-raw-additive{suffix}", GpuBlendMode.RawAdditive, false, false, samples,
|
||||||
|
shaders: baseShaders,
|
||||||
|
shaderName: baseShaderName,
|
||||||
|
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||||
Track(CreateMeshPipeline(
|
Track(CreateMeshPipeline(
|
||||||
device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples,
|
device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples,
|
||||||
shaders: baseShaders,
|
shaders: baseShaders,
|
||||||
shaderName: baseShaderName,
|
shaderName: baseShaderName,
|
||||||
|
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||||
|
Track(CreateMeshPipeline(
|
||||||
|
device, $"{namePrefix}-inverse-additive{suffix}", GpuBlendMode.InverseAdditive, false, false, samples,
|
||||||
|
shaders: baseShaders,
|
||||||
|
shaderName: baseShaderName,
|
||||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)));
|
usesRenderPackShaderAbi: usesRenderPackShaderAbi)));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
@ -370,9 +382,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
encoder,
|
encoder,
|
||||||
frame,
|
frame,
|
||||||
out DirectionalShadowFrameBinding shadowBinding);
|
out DirectionalShadowFrameBinding shadowBinding);
|
||||||
|
IGpuPipeline opaquePipeline = AlphaToCoverage
|
||||||
|
? pipelines.OpaqueAlphaToCoverage
|
||||||
|
: pipelines.Opaque;
|
||||||
BindPipelineWithMesh(
|
BindPipelineWithMesh(
|
||||||
encoder,
|
encoder,
|
||||||
AlphaToCoverage ? pipelines.OpaqueAlphaToCoverage : pipelines.Opaque,
|
opaquePipeline,
|
||||||
mesh);
|
mesh);
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
BindDirectionalShadowReceiver(encoder, in shadowBinding);
|
BindDirectionalShadowReceiver(encoder, in shadowBinding);
|
||||||
|
|
@ -436,7 +451,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
using (BeginRhiTimer(encoder, diag, OpaqueTimerScope))
|
using (BeginRhiTimer(encoder, diag, OpaqueTimerScope))
|
||||||
{
|
{
|
||||||
DrawDetailAwareRangeRhi(
|
DrawDetailAwareRangeRhi(
|
||||||
encoder, ref pushConstants, commandBuffer, commandBase,
|
encoder, mesh, pipelines, opaquePipeline,
|
||||||
|
ref pushConstants, commandBuffer, commandBase,
|
||||||
0, _opaqueDrawCount, usedDetailCategories, detailEnabled);
|
0, _opaqueDrawCount, usedDetailCategories, detailEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -634,7 +650,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
/// (M3c) precisely because detail-eligible content no longer reaches it.
|
/// (M3c) precisely because detail-eligible content no longer reaches it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void DrawImmediateAlphaInstanceRhi(
|
private void DrawImmediateAlphaInstanceRhi(
|
||||||
GlobalMeshBuffer mesh, TranslucencyKind blend, Matrix4x4 viewProjection)
|
GlobalMeshBuffer mesh,
|
||||||
|
TranslucencyKind blend,
|
||||||
|
RetailSetSurfaceMaterialState materialState,
|
||||||
|
Matrix4x4 viewProjection)
|
||||||
{
|
{
|
||||||
if (_alphaCommands.Buffer is null)
|
if (_alphaCommands.Buffer is null)
|
||||||
return;
|
return;
|
||||||
|
|
@ -644,9 +663,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
GpuPushConstants pushConstants = BindAlphaDrawState(
|
GpuPushConstants pushConstants = BindAlphaDrawState(
|
||||||
encoder, frame, mesh, viewProjection, out MeshPipelineSet pipelines);
|
encoder, frame, mesh, viewProjection, out MeshPipelineSet pipelines);
|
||||||
|
|
||||||
BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh);
|
BindPipelineWithMesh(
|
||||||
|
encoder,
|
||||||
|
PipelineForMaterial(pipelines, materialState, pipelines.Opaque),
|
||||||
|
mesh);
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
ArmBuildingDetail(ref pushConstants);
|
ArmBuildingDetail(ref pushConstants, materialState);
|
||||||
DrawIndirectRangeRhi(
|
DrawIndirectRangeRhi(
|
||||||
encoder,
|
encoder,
|
||||||
ref pushConstants,
|
ref pushConstants,
|
||||||
|
|
@ -673,6 +695,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
while (command < end)
|
while (command < end)
|
||||||
{
|
{
|
||||||
TranslucencyKind blend = _groupInputScratch[command].Translucency;
|
TranslucencyKind blend = _groupInputScratch[command].Translucency;
|
||||||
|
RetailSetSurfaceMaterialState materialState =
|
||||||
|
_groupInputScratch[command].MaterialState;
|
||||||
bool hasDetail = detailEnabled
|
bool hasDetail = detailEnabled
|
||||||
&& CommandContainsDetailCategory(
|
&& CommandContainsDetailCategory(
|
||||||
_indirectCommands[command],
|
_indirectCommands[command],
|
||||||
|
|
@ -691,10 +715,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
|
|
||||||
BindPipelineWithMesh(
|
BindPipelineWithMesh(
|
||||||
encoder,
|
encoder,
|
||||||
PipelineForBlend(pipelines, blend),
|
hasDetail
|
||||||
|
? PipelineForMaterial(pipelines, materialState, pipelines.Opaque)
|
||||||
|
: PipelineForBlend(pipelines, blend),
|
||||||
mesh);
|
mesh);
|
||||||
if (hasDetail)
|
if (hasDetail)
|
||||||
ArmBuildingDetail(ref pushConstants);
|
ArmBuildingDetail(ref pushConstants, materialState);
|
||||||
else
|
else
|
||||||
ClearDetailPushConstants(ref pushConstants);
|
ClearDetailPushConstants(ref pushConstants);
|
||||||
DrawIndirectRangeRhi(
|
DrawIndirectRangeRhi(
|
||||||
|
|
@ -712,6 +738,9 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
|
|
||||||
private void DrawDetailAwareRangeRhi(
|
private void DrawDetailAwareRangeRhi(
|
||||||
IGpuPassEncoder encoder,
|
IGpuPassEncoder encoder,
|
||||||
|
GlobalMeshBuffer mesh,
|
||||||
|
MeshPipelineSet pipelines,
|
||||||
|
IGpuPipeline opaquePipeline,
|
||||||
ref GpuPushConstants pushConstants,
|
ref GpuPushConstants pushConstants,
|
||||||
IGpuBuffer commandBuffer,
|
IGpuBuffer commandBuffer,
|
||||||
uint commandBase,
|
uint commandBase,
|
||||||
|
|
@ -738,9 +767,20 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDetail)
|
if (hasDetail)
|
||||||
ArmBuildingDetail(ref pushConstants);
|
{
|
||||||
|
RetailSetSurfaceMaterialState materialState =
|
||||||
|
_groupInputScratch[command].MaterialState;
|
||||||
|
BindPipelineWithMesh(
|
||||||
|
encoder,
|
||||||
|
PipelineForMaterial(pipelines, materialState, opaquePipeline),
|
||||||
|
mesh);
|
||||||
|
ArmBuildingDetail(ref pushConstants, materialState);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
BindPipelineWithMesh(encoder, opaquePipeline, mesh);
|
||||||
ClearDetailPushConstants(ref pushConstants);
|
ClearDetailPushConstants(ref pushConstants);
|
||||||
|
}
|
||||||
DrawIndirectRangeRhi(
|
DrawIndirectRangeRhi(
|
||||||
encoder,
|
encoder,
|
||||||
ref pushConstants,
|
ref pushConstants,
|
||||||
|
|
@ -754,10 +794,17 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ArmBuildingDetail(ref GpuPushConstants pushConstants)
|
private void ArmBuildingDetail(
|
||||||
|
ref GpuPushConstants pushConstants,
|
||||||
|
RetailSetSurfaceMaterialState materialState)
|
||||||
{
|
{
|
||||||
pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index;
|
pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index;
|
||||||
pushConstants.ParamA = _buildingDetail.Tiling;
|
pushConstants.ParamA = _buildingDetail.Tiling;
|
||||||
|
pushConstants.ParamB = materialState.AlphaTestReference;
|
||||||
|
if (materialState.FogEnabled)
|
||||||
|
pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag;
|
||||||
|
else
|
||||||
|
pushConstants.RenderPass |= RetailDetailTextureContract.NoFogRenderPassFlag;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ClearDetailPushConstants(
|
private static void ClearDetailPushConstants(
|
||||||
|
|
@ -766,6 +813,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
pushConstants.TextureIndexA = 0;
|
pushConstants.TextureIndexA = 0;
|
||||||
pushConstants.ParamA = 0f;
|
pushConstants.ParamA = 0f;
|
||||||
pushConstants.ParamB = 0f;
|
pushConstants.ParamB = 0f;
|
||||||
|
pushConstants.RenderPass &= ~RetailDetailTextureContract.NoFogRenderPassFlag;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IGpuPipeline PipelineForBlend(MeshPipelineSet pipelines, TranslucencyKind blend) =>
|
private static IGpuPipeline PipelineForBlend(MeshPipelineSet pipelines, TranslucencyKind blend) =>
|
||||||
|
|
@ -776,6 +824,23 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
_ => pipelines.AlphaBlend,
|
_ => pipelines.AlphaBlend,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static IGpuPipeline PipelineForMaterial(
|
||||||
|
MeshPipelineSet pipelines,
|
||||||
|
RetailSetSurfaceMaterialState material,
|
||||||
|
IGpuPipeline opaquePipeline) => material.Blend switch
|
||||||
|
{
|
||||||
|
RetailSetSurfaceBlend.Opaque => opaquePipeline,
|
||||||
|
RetailSetSurfaceBlend.StraightAlpha => pipelines.AlphaBlend,
|
||||||
|
RetailSetSurfaceBlend.AlphaAdditive => pipelines.AlphaAdditive,
|
||||||
|
RetailSetSurfaceBlend.Additive => pipelines.RawAdditive,
|
||||||
|
RetailSetSurfaceBlend.InverseAlpha => pipelines.AlphaInverse,
|
||||||
|
RetailSetSurfaceBlend.InverseAdditive => pipelines.InverseAdditive,
|
||||||
|
// AP-240: ordinary Gfx/building pure Clip stays on its selected
|
||||||
|
// opaque/A2C pipeline; exact final-X ParamB still reaches shader.
|
||||||
|
RetailSetSurfaceBlend.Clip => opaquePipeline,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(material), material, "Unknown SetSurface blend."),
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads cull modes from <paramref name="cullModes"/> when the caller
|
/// Reads cull modes from <paramref name="cullModes"/> when the caller
|
||||||
/// supplies one, or from the shared <see cref="_drawCullModes"/> scratch
|
/// supplies one, or from the shared <see cref="_drawCullModes"/> scratch
|
||||||
|
|
@ -1102,7 +1167,9 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
pipelines.OpaqueAlphaToCoverage.Dispose();
|
pipelines.OpaqueAlphaToCoverage.Dispose();
|
||||||
pipelines.AlphaBlend.Dispose();
|
pipelines.AlphaBlend.Dispose();
|
||||||
pipelines.AlphaAdditive.Dispose();
|
pipelines.AlphaAdditive.Dispose();
|
||||||
|
pipelines.RawAdditive.Dispose();
|
||||||
pipelines.AlphaInverse.Dispose();
|
pipelines.AlphaInverse.Dispose();
|
||||||
|
pipelines.InverseAdditive.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class NullRhiTimerScope : IDisposable
|
private sealed class NullRhiTimerScope : IDisposable
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
key = new GroupKey(
|
key = new GroupKey(
|
||||||
batch.FirstIndex, (int)batch.BaseVertex,
|
batch.FirstIndex, (int)batch.BaseVertex,
|
||||||
batch.IndexCount, texture.Slot, texture.Layer, translucency,
|
batch.IndexCount, texture.Slot, texture.Layer, translucency,
|
||||||
|
MaterialState: batch.MaterialState,
|
||||||
FoliageFlags: foliageFlags,
|
FoliageFlags: foliageFlags,
|
||||||
SurfaceOpacity: batch.SurfaceOpacity,
|
SurfaceOpacity: batch.SurfaceOpacity,
|
||||||
CullMode: batch.CullMode);
|
CullMode: batch.CullMode);
|
||||||
|
|
|
||||||
|
|
@ -2050,6 +2050,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
TextureIndex: g.TextureSlot.Index,
|
TextureIndex: g.TextureSlot.Index,
|
||||||
TextureLayer: g.TextureLayer,
|
TextureLayer: g.TextureLayer,
|
||||||
Translucency: g.Translucency,
|
Translucency: g.Translucency,
|
||||||
|
MaterialState: g.MaterialState,
|
||||||
SurfaceOpacity: g.SurfaceOpacity,
|
SurfaceOpacity: g.SurfaceOpacity,
|
||||||
CullMode: g.CullMode,
|
CullMode: g.CullMode,
|
||||||
FoliageFlags: g.FoliageFlags);
|
FoliageFlags: g.FoliageFlags);
|
||||||
|
|
@ -2123,6 +2124,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
g.TextureSlot,
|
g.TextureSlot,
|
||||||
g.TextureLayer,
|
g.TextureLayer,
|
||||||
g.Translucency,
|
g.Translucency,
|
||||||
|
g.MaterialState,
|
||||||
g.FoliageFlags,
|
g.FoliageFlags,
|
||||||
g.SurfaceOpacity,
|
g.SurfaceOpacity,
|
||||||
g.CullMode);
|
g.CullMode);
|
||||||
|
|
@ -2144,6 +2146,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
TextureSlot = key.TextureSlot,
|
TextureSlot = key.TextureSlot,
|
||||||
TextureLayer = key.TextureLayer,
|
TextureLayer = key.TextureLayer,
|
||||||
Translucency = key.Translucency,
|
Translucency = key.Translucency,
|
||||||
|
MaterialState = key.MaterialState,
|
||||||
CullMode = key.CullMode,
|
CullMode = key.CullMode,
|
||||||
FoliageFlags = key.FoliageFlags,
|
FoliageFlags = key.FoliageFlags,
|
||||||
SurfaceOpacity = key.SurfaceOpacity,
|
SurfaceOpacity = key.SurfaceOpacity,
|
||||||
|
|
@ -2641,7 +2644,11 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
EnsureDeferredAlphaCapacity(1);
|
EnsureDeferredAlphaCapacity(1);
|
||||||
WriteDeferredAlphaEntrySlot(0, in entry);
|
WriteDeferredAlphaEntrySlot(0, in entry);
|
||||||
PrepareRhiAlphaSections(1);
|
PrepareRhiAlphaSections(1);
|
||||||
DrawImmediateAlphaInstanceRhi(global, entry.Key.Translucency, viewProjection);
|
DrawImmediateAlphaInstanceRhi(
|
||||||
|
global,
|
||||||
|
entry.Key.Translucency,
|
||||||
|
entry.Key.MaterialState,
|
||||||
|
viewProjection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
|
||||||
|
|
@ -3437,6 +3444,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
uint TextureIndex,
|
uint TextureIndex,
|
||||||
uint TextureLayer,
|
uint TextureLayer,
|
||||||
TranslucencyKind Translucency,
|
TranslucencyKind Translucency,
|
||||||
|
RetailSetSurfaceMaterialState MaterialState,
|
||||||
float SurfaceOpacity = 1f,
|
float SurfaceOpacity = 1f,
|
||||||
CullMode CullMode = CullMode.CounterClockwise,
|
CullMode CullMode = CullMode.CounterClockwise,
|
||||||
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
|
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
|
||||||
|
|
@ -3675,6 +3683,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
|
AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
|
||||||
public uint TextureLayer; // Layer in either the pooled composite array or WB shared atlas.
|
public uint TextureLayer; // Layer in either the pooled composite array or WB shared atlas.
|
||||||
public TranslucencyKind Translucency;
|
public TranslucencyKind Translucency;
|
||||||
|
public RetailSetSurfaceMaterialState MaterialState =
|
||||||
|
RetailSetSurfaceMaterialState.Opaque;
|
||||||
public float SurfaceOpacity = 1f;
|
public float SurfaceOpacity = 1f;
|
||||||
public CullMode CullMode;
|
public CullMode CullMode;
|
||||||
public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes)
|
public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes)
|
||||||
|
|
|
||||||
|
|
@ -396,6 +396,7 @@ public sealed class MeshExtractor {
|
||||||
bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type);
|
bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type);
|
||||||
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
|
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
|
||||||
uint paletteId = 0;
|
uint paletteId = 0;
|
||||||
|
bool texturePresent = false;
|
||||||
bool isDxt3or5 = false;
|
bool isDxt3or5 = false;
|
||||||
bool textureDataIsCached = false;
|
bool textureDataIsCached = false;
|
||||||
DatReaderWriter.Enums.PixelFormat? sourceFormat = null;
|
DatReaderWriter.Enums.PixelFormat? sourceFormat = null;
|
||||||
|
|
@ -410,6 +411,7 @@ public sealed class MeshExtractor {
|
||||||
textureDataIsCached = true;
|
textureDataIsCached = true;
|
||||||
}
|
}
|
||||||
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
|
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
|
||||||
|
texturePresent = true;
|
||||||
var renderSurfaceId = surfaceTexture.Textures.First();
|
var renderSurfaceId = surfaceTexture.Textures.First();
|
||||||
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var renderSurface)) {
|
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var renderSurface)) {
|
||||||
// check highres
|
// check highres
|
||||||
|
|
@ -603,6 +605,10 @@ public sealed class MeshExtractor {
|
||||||
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
|
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
|
||||||
surface.Type,
|
surface.Type,
|
||||||
surface.Translucency),
|
surface.Translucency),
|
||||||
|
MaterialState = RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
surface.Type,
|
||||||
|
texturePresent,
|
||||||
|
paletteId != 0),
|
||||||
IsTransparent = isTransparent,
|
IsTransparent = isTransparent,
|
||||||
IsAdditive = isAdditive
|
IsAdditive = isAdditive
|
||||||
};
|
};
|
||||||
|
|
@ -900,6 +906,7 @@ public sealed class MeshExtractor {
|
||||||
bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type);
|
bool isSolid = RetailUntexturedSurfacePolicy.IsUntextured(surface.Type);
|
||||||
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
|
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
|
||||||
uint paletteId = 0;
|
uint paletteId = 0;
|
||||||
|
bool texturePresent = false;
|
||||||
bool isDxt3or5 = false;
|
bool isDxt3or5 = false;
|
||||||
bool textureDataIsCached = false;
|
bool textureDataIsCached = false;
|
||||||
DatReaderWriter.Enums.PixelFormat? sourceFormat = null;
|
DatReaderWriter.Enums.PixelFormat? sourceFormat = null;
|
||||||
|
|
@ -914,6 +921,7 @@ public sealed class MeshExtractor {
|
||||||
textureDataIsCached = true;
|
textureDataIsCached = true;
|
||||||
}
|
}
|
||||||
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
|
else if (_dats.Portal.TryGet<SurfaceTexture>(surface.OrigTextureId, out var surfaceTexture)) {
|
||||||
|
texturePresent = true;
|
||||||
var renderSurfaceId = surfaceTexture.Textures.First();
|
var renderSurfaceId = surfaceTexture.Textures.First();
|
||||||
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var renderSurface)) {
|
if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var renderSurface)) {
|
||||||
if (!_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out var hrRenderSurface)) {
|
if (!_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out var hrRenderSurface)) {
|
||||||
|
|
@ -1080,6 +1088,10 @@ public sealed class MeshExtractor {
|
||||||
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
|
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
|
||||||
surface.Type,
|
surface.Type,
|
||||||
surface.Translucency),
|
surface.Translucency),
|
||||||
|
MaterialState = RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
surface.Type,
|
||||||
|
texturePresent,
|
||||||
|
paletteId != 0),
|
||||||
IsTransparent = isTransparent,
|
IsTransparent = isTransparent,
|
||||||
IsAdditive = isAdditive,
|
IsAdditive = isAdditive,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,13 @@ public class TextureBatchData {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float SurfaceOpacity { get; set; } = 1f;
|
public float SurfaceOpacity { get; set; } = 1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact retail SetSurface blend/alpha-test/fog state resolved during
|
||||||
|
/// extraction from the source Surface and texture palette class.
|
||||||
|
/// </summary>
|
||||||
|
public RetailSetSurfaceMaterialState MaterialState { get; set; } =
|
||||||
|
RetailSetSurfaceMaterialState.Opaque;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retail source surface-array index this subset was constructed from
|
/// Retail source surface-array index this subset was constructed from
|
||||||
/// (contract §3.6) — the CellStruct subset/material OWNER, not the
|
/// (contract §3.6) — the CellStruct subset/material OWNER, not the
|
||||||
|
|
|
||||||
|
|
@ -228,9 +228,13 @@ public static class ObjectMeshDataSerializer {
|
||||||
w.Write(batch.RetailSurfaceMask);
|
w.Write(batch.RetailSurfaceMask);
|
||||||
w.Write(batch.RawSurfaceType);
|
w.Write(batch.RawSurfaceType);
|
||||||
w.Write(batch.IsCellShell);
|
w.Write(batch.IsCellShell);
|
||||||
// OVERHAUL S5-c4: recipe 9 appends authored material opacity. Recipe
|
// OVERHAUL S5-c4: recipe 9 appended authored material opacity. Recipe
|
||||||
|
// 10 appends the extraction-resolved SetSurface state byte. Recipe
|
||||||
// identity rejects older payloads before this serializer is entered.
|
// identity rejects older payloads before this serializer is entered.
|
||||||
w.Write(batch.SurfaceOpacity);
|
w.Write(batch.SurfaceOpacity);
|
||||||
|
// S5-c4 fix round 1: recipe 10 appends the immutable resolved
|
||||||
|
// SetSurface state as one validated deterministic byte.
|
||||||
|
w.Write(batch.MaterialState.ToPackedByte());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TextureBatchData ReadTextureBatchData(
|
private static TextureBatchData ReadTextureBatchData(
|
||||||
|
|
@ -256,6 +260,9 @@ public static class ObjectMeshDataSerializer {
|
||||||
batch.RawSurfaceType = r.ReadUInt32();
|
batch.RawSurfaceType = r.ReadUInt32();
|
||||||
batch.IsCellShell = r.ReadBoolean();
|
batch.IsCellShell = r.ReadBoolean();
|
||||||
batch.SurfaceOpacity = r.ReadSingle();
|
batch.SurfaceOpacity = r.ReadSingle();
|
||||||
|
batch.MaterialState =
|
||||||
|
AcDream.Core.Meshing.RetailSetSurfaceMaterialState.FromPackedByte(
|
||||||
|
r.ReadByte());
|
||||||
return batch;
|
return batch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,11 @@ public static class PakFormat {
|
||||||
/// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after
|
/// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after
|
||||||
/// surface resolution. Version 9 appends authored surface opacity to
|
/// surface resolution. Version 9 appends authored surface opacity to
|
||||||
/// every prepared mesh texture batch. The binary format remains version
|
/// every prepared mesh texture batch. The binary format remains version
|
||||||
/// 2, but every prepared render record must be regenerated.
|
/// 2, but every prepared render record must be regenerated. Version 10
|
||||||
|
/// appends the exact resolved SetSurface blend, alpha-test, and fog state
|
||||||
|
/// to every prepared texture batch; recipe 9 cannot be read as recipe 10.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const uint CurrentBakeToolVersion = 9;
|
public const uint CurrentBakeToolVersion = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,135 @@ public enum TranslucencyKind
|
||||||
InvAlpha = 4,
|
InvAlpha = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact framebuffer blend selected by retail's
|
||||||
|
/// <c>D3DPolyRender::SetSurface</c>. This is deliberately separate from
|
||||||
|
/// <see cref="TranslucencyKind"/>, which remains the existing queue-membership
|
||||||
|
/// classification and cannot distinguish the additive factor pairs.
|
||||||
|
/// </summary>
|
||||||
|
public enum RetailSetSurfaceBlend : byte
|
||||||
|
{
|
||||||
|
Opaque = 0,
|
||||||
|
StraightAlpha = 1,
|
||||||
|
AlphaAdditive = 2,
|
||||||
|
Additive = 3,
|
||||||
|
InverseAlpha = 4,
|
||||||
|
InverseAdditive = 5,
|
||||||
|
Clip = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The exact final-alpha comparison selected by SetSurface.</summary>
|
||||||
|
public enum RetailSetSurfaceAlphaTest : byte
|
||||||
|
{
|
||||||
|
Disabled = 0,
|
||||||
|
Paletted = 1,
|
||||||
|
Dds = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable resolved SetSurface state prepared from the source Surface and
|
||||||
|
/// its actual texture/palette class. Renderers consume this value directly;
|
||||||
|
/// they never reconstruct it from queue kind or texture identity.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct RetailSetSurfaceMaterialState(
|
||||||
|
RetailSetSurfaceBlend Blend,
|
||||||
|
RetailSetSurfaceAlphaTest AlphaTest,
|
||||||
|
bool FogEnabled)
|
||||||
|
{
|
||||||
|
public static RetailSetSurfaceMaterialState Opaque { get; } = new(
|
||||||
|
RetailSetSurfaceBlend.Opaque,
|
||||||
|
RetailSetSurfaceAlphaTest.Disabled,
|
||||||
|
FogEnabled: true);
|
||||||
|
|
||||||
|
public bool AlphaTestEnabled => AlphaTest != RetailSetSurfaceAlphaTest.Disabled;
|
||||||
|
|
||||||
|
public float AlphaTestReference => AlphaTest switch
|
||||||
|
{
|
||||||
|
RetailSetSurfaceAlphaTest.Disabled => 0f,
|
||||||
|
RetailSetSurfaceAlphaTest.Paletted => 100f / 255f,
|
||||||
|
RetailSetSurfaceAlphaTest.Dds => 200f / 255f,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(AlphaTest), AlphaTest, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>One deterministic recipe byte; bits 6-7 remain reserved.</summary>
|
||||||
|
public byte ToPackedByte()
|
||||||
|
{
|
||||||
|
if ((uint)Blend > (uint)RetailSetSurfaceBlend.Clip)
|
||||||
|
throw new InvalidOperationException($"Unknown SetSurface blend {Blend}.");
|
||||||
|
if ((uint)AlphaTest > (uint)RetailSetSurfaceAlphaTest.Dds)
|
||||||
|
throw new InvalidOperationException($"Unknown SetSurface alpha test {AlphaTest}.");
|
||||||
|
return (byte)((byte)Blend | ((byte)AlphaTest << 3) | (FogEnabled ? 0 : 0x20));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RetailSetSurfaceMaterialState FromPackedByte(byte value)
|
||||||
|
{
|
||||||
|
if ((value & 0xC0) != 0)
|
||||||
|
throw new InvalidDataException($"Reserved SetSurface state bits are set: 0x{value:X2}.");
|
||||||
|
var blend = (RetailSetSurfaceBlend)(value & 0x07);
|
||||||
|
var alphaTest = (RetailSetSurfaceAlphaTest)((value >> 3) & 0x03);
|
||||||
|
if ((uint)blend > (uint)RetailSetSurfaceBlend.Clip
|
||||||
|
|| (uint)alphaTest > (uint)RetailSetSurfaceAlphaTest.Dds)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Invalid SetSurface state byte: 0x{value:X2}.");
|
||||||
|
}
|
||||||
|
return new RetailSetSurfaceMaterialState(
|
||||||
|
blend,
|
||||||
|
alphaTest,
|
||||||
|
FogEnabled: (value & 0x20) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RetailSetSurfaceMaterialState Resolve(
|
||||||
|
SurfaceType type,
|
||||||
|
bool texturePresent,
|
||||||
|
bool textureHasPalette)
|
||||||
|
{
|
||||||
|
bool additive = (type & SurfaceType.Additive) != 0;
|
||||||
|
bool alpha = (type & SurfaceType.Alpha) != 0;
|
||||||
|
bool inverse = (type & SurfaceType.InvAlpha) != 0;
|
||||||
|
bool clip = (type & SurfaceType.Base1ClipMap) != 0;
|
||||||
|
bool translucent = (type & SurfaceType.Translucent) != 0;
|
||||||
|
|
||||||
|
RetailSetSurfaceBlend blend = alpha
|
||||||
|
? additive
|
||||||
|
? RetailSetSurfaceBlend.AlphaAdditive
|
||||||
|
: RetailSetSurfaceBlend.StraightAlpha
|
||||||
|
: inverse
|
||||||
|
? additive
|
||||||
|
? RetailSetSurfaceBlend.InverseAdditive
|
||||||
|
: RetailSetSurfaceBlend.InverseAlpha
|
||||||
|
: additive
|
||||||
|
? RetailSetSurfaceBlend.Additive
|
||||||
|
: RetailSetSurfaceBlend.Opaque;
|
||||||
|
|
||||||
|
RetailSetSurfaceAlphaTest alphaTest = RetailSetSurfaceAlphaTest.Disabled;
|
||||||
|
if (clip)
|
||||||
|
{
|
||||||
|
alphaTest = texturePresent && textureHasPalette
|
||||||
|
? RetailSetSurfaceAlphaTest.Paletted
|
||||||
|
: RetailSetSurfaceAlphaTest.Dds;
|
||||||
|
if (blend == RetailSetSurfaceBlend.Opaque)
|
||||||
|
blend = RetailSetSurfaceBlend.Clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retail's later Translucent arm forces every ClipMap combination and
|
||||||
|
// the otherwise-opaque/straight cases to straight alpha, disabling the
|
||||||
|
// ClipMap test. Established additive/inverse families remain intact.
|
||||||
|
if (translucent
|
||||||
|
&& (clip
|
||||||
|
|| blend is RetailSetSurfaceBlend.Opaque
|
||||||
|
or RetailSetSurfaceBlend.StraightAlpha))
|
||||||
|
{
|
||||||
|
blend = RetailSetSurfaceBlend.StraightAlpha;
|
||||||
|
alphaTest = RetailSetSurfaceAlphaTest.Disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RetailSetSurfaceMaterialState(
|
||||||
|
blend,
|
||||||
|
alphaTest,
|
||||||
|
FogEnabled: !additive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class TranslucencyKindExtensions
|
public static class TranslucencyKindExtensions
|
||||||
{
|
{
|
||||||
// Translucent override comes FIRST, then the existing priority chain:
|
// Translucent override comes FIRST, then the existing priority chain:
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,10 @@ public static class ContentMigrationCatalog
|
||||||
8,
|
8,
|
||||||
9,
|
9,
|
||||||
"authored surface opacity in prepared mesh batches"),
|
"authored surface opacity in prepared mesh batches"),
|
||||||
|
[10] = FullRebuild(
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
"exact resolved SetSurface state in prepared mesh batches"),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static ContentMigrationPlan Resolve(uint fromRecipeVersion, uint targetRecipeVersion)
|
public static ContentMigrationPlan Resolve(uint fromRecipeVersion, uint targetRecipeVersion)
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,10 @@ public sealed class LauncherInstallRecordStore
|
||||||
// with retail's authored DrawingBSP view sphere. Version 8 (OH2/S1) keeps
|
// with retail's authored DrawingBSP view sphere. Version 8 (OH2/S1) keeps
|
||||||
// pak format 2 and regenerates every CellStruct/EnvCell render record
|
// pak format 2 and regenerates every CellStruct/EnvCell render record
|
||||||
// with retail's exact surface-array-index subset construction. Version 9
|
// with retail's exact surface-array-index subset construction. Version 9
|
||||||
// carries authored surface opacity in prepared mesh batches. Each is a
|
// carries authored surface opacity in prepared mesh batches. Version 10
|
||||||
// mandatory full rebuild from its predecessor recipe.
|
// adds exact resolved SetSurface state. Each is a mandatory full rebuild
|
||||||
public const uint CurrentBakeToolVersion = 9;
|
// from its predecessor recipe.
|
||||||
|
public const uint CurrentBakeToolVersion = 10;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ using AcDream.App.Rendering.Gpu.Vk;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Tests.Rendering.Gpu;
|
using AcDream.App.Tests.Rendering.Gpu;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
using Chorizite.Core.Render.Enums;
|
using Chorizite.Core.Render.Enums;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using CullMode = DatReaderWriter.Enums.CullMode;
|
using CullMode = DatReaderWriter.Enums.CullMode;
|
||||||
|
|
||||||
|
|
@ -21,6 +23,11 @@ namespace AcDream.App.Tests.Rendering;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EnvCellAlphaDrawSourceTests
|
public sealed class EnvCellAlphaDrawSourceTests
|
||||||
{
|
{
|
||||||
|
private static readonly Vector4 MaterialBase = new(0.31f, 0.57f, 0.83f, 0.19f);
|
||||||
|
private static readonly Vector3 MaterialDiffuse = new(0.73f, 0.41f, 0.67f);
|
||||||
|
private static readonly Vector4 MaterialDetail = new(0.91f, 0.23f, 0.49f, 0.62f);
|
||||||
|
private static readonly Vector4 MaterialDestination = new(0.17f, 0.37f, 0.71f, 0.29f);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Canonical F4180104 surface 08000BFF's pure Base1ClipMap mask 0x08
|
/// Canonical F4180104 surface 08000BFF's pure Base1ClipMap mask 0x08
|
||||||
/// reaches CLIP while alpha-family 0x02 reaches ALPHA. Mutation check:
|
/// reaches CLIP while alpha-family 0x02 reaches ALPHA. Mutation check:
|
||||||
|
|
@ -75,26 +82,45 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
DrawPipelineNames(fixture.Device));
|
DrawPipelineNames(fixture.Device));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
public static TheoryData<SurfaceType, bool, string, object, float, bool> ExactMaterialRows() => new()
|
||||||
[InlineData(0, "envcell-opaque", 0f)]
|
|
||||||
[InlineData(1, "envcell-alpha", 0f)]
|
|
||||||
[InlineData(2, "envcell-additive", 0f)]
|
|
||||||
[InlineData(3, "envcell-clip", 200f / 255f)]
|
|
||||||
[InlineData(4, "envcell-clip", 100f / 255f)]
|
|
||||||
public void DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity(
|
|
||||||
int specIndex,
|
|
||||||
string expectedPipeline,
|
|
||||||
float expectedReference)
|
|
||||||
{
|
{
|
||||||
BatchSpec spec = specIndex switch
|
{ SurfaceType.Base1Image, false, "envcell-opaque", GpuBlendMode.None, 0f, true },
|
||||||
{
|
{ SurfaceType.Base1Image | SurfaceType.Alpha, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, true },
|
||||||
0 => BatchSpec.Opaque,
|
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false, "envcell-additive", GpuBlendMode.Additive, 0f, false },
|
||||||
1 => BatchSpec.Alpha,
|
{ SurfaceType.Base1Image | SurfaceType.Additive, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 0f, false },
|
||||||
2 => BatchSpec.Additive,
|
{ SurfaceType.Base1Image | SurfaceType.InvAlpha, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 0f, true },
|
||||||
3 => BatchSpec.ClipDds,
|
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive, false, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 0f, false },
|
||||||
4 => BatchSpec.ClipPaletted,
|
{ SurfaceType.Translucent, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, true },
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(specIndex)),
|
{ SurfaceType.Translucent | SurfaceType.Additive, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 0f, false },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 0f, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, false, "envcell-clip", GpuBlendMode.PremultipliedAlpha, 200f / 255f, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, true, "envcell-clip", GpuBlendMode.PremultipliedAlpha, 100f / 255f, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 200f / 255f, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "envcell-alpha", GpuBlendMode.StraightAlpha, 100f / 255f, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-additive", GpuBlendMode.Additive, 200f / 255f, false },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-additive", GpuBlendMode.Additive, 100f / 255f, false },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 200f / 255f, false },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-raw-additive", GpuBlendMode.RawAdditive, 100f / 255f, false },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 200f / 255f, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, true, "envcell-inverse", GpuBlendMode.InverseAlpha, 100f / 255f, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 200f / 255f, false },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 100f / 255f, false },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, false },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, false },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ExactMaterialRows))]
|
||||||
|
public void DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity(
|
||||||
|
SurfaceType surfaceType,
|
||||||
|
bool paletted,
|
||||||
|
string expectedPipeline,
|
||||||
|
object expectedBlendValue,
|
||||||
|
float expectedReference,
|
||||||
|
bool expectedFog)
|
||||||
|
{
|
||||||
|
var expectedBlend = (GpuBlendMode)expectedBlendValue;
|
||||||
|
BatchSpec spec = new(surfaceType, paletted, PositiveStippling: false);
|
||||||
using var fixture = new ProductionEnvCellFixture(
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
detailSurfaceActive: true,
|
detailSurfaceActive: true,
|
||||||
spec);
|
spec);
|
||||||
|
|
@ -113,6 +139,20 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
DrawPushConstants(fixture.Device),
|
DrawPushConstants(fixture.Device),
|
||||||
constants => constants.ParamA != 0f);
|
constants => constants.ParamA != 0f);
|
||||||
Assert.NotEqual(0u, armed.TextureIndexA);
|
Assert.NotEqual(0u, armed.TextureIndexA);
|
||||||
|
Assert.Equal(
|
||||||
|
!expectedFog,
|
||||||
|
(armed.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag) != 0);
|
||||||
|
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
surfaceType, texturePresent: true, textureHasPalette: paletted);
|
||||||
|
GpuPipelineDescription selected = fixture.Device.CreatedPipelines
|
||||||
|
.Single(pipeline => pipeline.Description.Name == expectedPipeline)
|
||||||
|
.Description;
|
||||||
|
Assert.Equal(expectedBlend, selected.Blend);
|
||||||
|
Assert.True(selected.Depth.Test);
|
||||||
|
Assert.Equal(
|
||||||
|
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
|
||||||
|
selected.Depth.Write);
|
||||||
|
Assert.Equal(WorldDepthContract.WorldCompare, selected.Depth.Compare);
|
||||||
Assert.Equal(0, fixture.Queue.PendingCount);
|
Assert.Equal(0, fixture.Queue.PendingCount);
|
||||||
GpuRecordedStorageBind batchBind = fixture.Device.Calls
|
GpuRecordedStorageBind batchBind = fixture.Device.Calls
|
||||||
.OfType<GpuRecordedStorageBind>()
|
.OfType<GpuRecordedStorageBind>()
|
||||||
|
|
@ -120,10 +160,77 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
ReadOnlySpan<ModernBatchData> gpuBatches = MemoryMarshal.Cast<byte, ModernBatchData>(
|
ReadOnlySpan<ModernBatchData> gpuBatches = MemoryMarshal.Cast<byte, ModernBatchData>(
|
||||||
fixture.Device.RingBytes.Slice((int)batchBind.OffsetBytes, (int)batchBind.SizeBytes));
|
fixture.Device.RingBytes.Slice((int)batchBind.OffsetBytes, (int)batchBind.SizeBytes));
|
||||||
Assert.Equal(0.25f, Assert.Single(gpuBatches.ToArray()).SurfaceOpacity);
|
Assert.Equal(0.25f, Assert.Single(gpuBatches.ToArray()).SurfaceOpacity);
|
||||||
|
Vector4 source = RetailDetailTextureContract.Combine(
|
||||||
|
MaterialBase, MaterialDiffuse, MaterialDetail, 0.75f, 0.4f);
|
||||||
|
AssertVector(new Vector4(0.3534682f, 0.2330118f, 0.5438054f, 0.11532f), source);
|
||||||
|
RetailDetailTextureContract.FramebufferFamily family = expectedBlend switch
|
||||||
|
{
|
||||||
|
GpuBlendMode.None => RetailDetailTextureContract.FramebufferFamily.Opaque,
|
||||||
|
GpuBlendMode.StraightAlpha => RetailDetailTextureContract.FramebufferFamily.Alpha,
|
||||||
|
GpuBlendMode.Additive => RetailDetailTextureContract.FramebufferFamily.AlphaAdditive,
|
||||||
|
GpuBlendMode.RawAdditive => RetailDetailTextureContract.FramebufferFamily.Additive,
|
||||||
|
GpuBlendMode.InverseAlpha => RetailDetailTextureContract.FramebufferFamily.InverseAlpha,
|
||||||
|
GpuBlendMode.InverseAdditive => RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive,
|
||||||
|
GpuBlendMode.PremultipliedAlpha => RetailDetailTextureContract.FramebufferFamily.Clip,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(expectedBlend)),
|
||||||
|
};
|
||||||
|
AssertVector(
|
||||||
|
IndependentComposite(source, MaterialDestination, family),
|
||||||
|
RetailDetailTextureContract.Composite(source, MaterialDestination, family));
|
||||||
|
if (resolved.AlphaTestEnabled)
|
||||||
|
{
|
||||||
|
Assert.False(RetailDetailTextureContract.SurvivesClip(
|
||||||
|
MathF.BitDecrement(expectedReference), expectedReference));
|
||||||
|
Assert.True(RetailDetailTextureContract.SurvivesClip(expectedReference, expectedReference));
|
||||||
|
Assert.True(RetailDetailTextureContract.SurvivesClip(
|
||||||
|
MathF.BitIncrement(expectedReference), expectedReference));
|
||||||
|
}
|
||||||
fixture.Queue.EndFrame();
|
fixture.Queue.EndFrame();
|
||||||
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static TheoryData<SurfaceType, bool, string, float> DetailOffRows() => new()
|
||||||
|
{
|
||||||
|
{ SurfaceType.Base1Image, false, "envcell-opaque", 0f },
|
||||||
|
{ SurfaceType.Alpha, false, "envcell-alpha", 0f },
|
||||||
|
{ SurfaceType.Alpha | SurfaceType.Additive, false, "envcell-additive", 0f },
|
||||||
|
{ SurfaceType.Additive, false, "envcell-additive", 0f },
|
||||||
|
{ SurfaceType.InvAlpha, false, "envcell-alpha", 0f },
|
||||||
|
{ SurfaceType.InvAlpha | SurfaceType.Additive, false, "envcell-additive", 0f },
|
||||||
|
{ SurfaceType.Base1ClipMap, false, "envcell-clip", 200f / 255f },
|
||||||
|
{ SurfaceType.Base1ClipMap, true, "envcell-clip", 100f / 255f },
|
||||||
|
{ SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "envcell-alpha", 0f },
|
||||||
|
{ SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "envcell-alpha", 0f },
|
||||||
|
{ SurfaceType.Translucent, false, "envcell-alpha", 0f },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.Additive, false, "envcell-additive", 0f },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, "envcell-alpha", 0f },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, "envcell-additive", 0f },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(DetailOffRows))]
|
||||||
|
public void DetailOff_EveryRawStateRetainsThePreFixLogicalPath(
|
||||||
|
SurfaceType surfaceType,
|
||||||
|
bool paletted,
|
||||||
|
string expectedPipeline,
|
||||||
|
float expectedReference)
|
||||||
|
{
|
||||||
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
|
detailSurfaceActive: false,
|
||||||
|
new BatchSpec(surfaceType, paletted, PositiveStippling: false));
|
||||||
|
fixture.Queue.BeginFrame();
|
||||||
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||||
|
fixture.Queue.EndFrame();
|
||||||
|
|
||||||
|
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
|
Assert.Equal([expectedPipeline], DrawPipelineNames(fixture.Device));
|
||||||
|
GpuPushConstants constants = Assert.Single(DrawPushConstants(fixture.Device));
|
||||||
|
Assert.Equal(0u, constants.TextureIndexA);
|
||||||
|
Assert.Equal(0f, constants.ParamA);
|
||||||
|
Assert.Equal(expectedReference, constants.ParamB);
|
||||||
|
Assert.Equal(0, constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
||||||
/// with an unrelated source's entry appended between them, must still
|
/// with an unrelated source's entry appended between them, must still
|
||||||
/// produce TWO separate single-cell draw calls around the interposed
|
/// produce TWO separate single-cell draw calls around the interposed
|
||||||
|
|
@ -258,9 +365,10 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
foreach (string shader in new[] { modern, atmospheric })
|
foreach (string shader in new[] { modern, atmospheric })
|
||||||
{
|
{
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)",
|
"? isRetailClipReference(uParamB) && alpha < uParamB",
|
||||||
shader,
|
shader,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
Assert.Contains("(uRenderPass & 0x200) == 0", shader, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("alpha <= alphaCutoff", shader, StringComparison.Ordinal);
|
Assert.DoesNotContain("alpha <= alphaCutoff", shader, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("color.a <= alphaCutoff", shader, StringComparison.Ordinal);
|
Assert.DoesNotContain("color.a <= alphaCutoff", shader, StringComparison.Ordinal);
|
||||||
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", shader, StringComparison.Ordinal);
|
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", shader, StringComparison.Ordinal);
|
||||||
|
|
@ -409,6 +517,38 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
return [.. constants];
|
return [.. constants];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AssertVector(Vector4 expected, Vector4 actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.X, actual.X, 6);
|
||||||
|
Assert.Equal(expected.Y, actual.Y, 6);
|
||||||
|
Assert.Equal(expected.Z, actual.Z, 6);
|
||||||
|
Assert.Equal(expected.W, actual.W, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 IndependentComposite(
|
||||||
|
Vector4 source,
|
||||||
|
Vector4 destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily family)
|
||||||
|
{
|
||||||
|
float x = source.W;
|
||||||
|
return family switch
|
||||||
|
{
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Opaque => source,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Alpha =>
|
||||||
|
source * x + destination * (1f - x),
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive =>
|
||||||
|
source * x + destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Additive => source + destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.InverseAlpha =>
|
||||||
|
source * (1f - x) + destination * x,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive =>
|
||||||
|
source * (1f - x) + destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Clip =>
|
||||||
|
source + destination * new Vector4(1f - x),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(family)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string RepositoryRoot()
|
private static string RepositoryRoot()
|
||||||
{
|
{
|
||||||
DirectoryInfo? cursor = new(AppContext.BaseDirectory);
|
DirectoryInfo? cursor = new(AppContext.BaseDirectory);
|
||||||
|
|
@ -419,46 +559,32 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct BatchSpec(
|
private readonly record struct BatchSpec(
|
||||||
byte Mask,
|
SurfaceType Type,
|
||||||
bool IsTransparent,
|
bool Paletted,
|
||||||
AcDream.Core.Meshing.TranslucencyKind Translucency,
|
bool PositiveStippling)
|
||||||
uint PaletteId)
|
|
||||||
{
|
{
|
||||||
internal static BatchSpec Opaque { get; } = new(
|
internal static BatchSpec Opaque { get; } = new(
|
||||||
0x00,
|
SurfaceType.Base1Image, Paletted: false, PositiveStippling: false);
|
||||||
IsTransparent: false,
|
|
||||||
AcDream.Core.Meshing.TranslucencyKind.Opaque,
|
|
||||||
PaletteId: 0);
|
|
||||||
|
|
||||||
internal static BatchSpec ClipDds { get; } = new(
|
internal static BatchSpec ClipDds { get; } = new(
|
||||||
RetailAlphaMeshRouter.MaskClipMap,
|
SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
|
||||||
IsTransparent: true,
|
Paletted: false, PositiveStippling: false);
|
||||||
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
|
||||||
PaletteId: 0);
|
|
||||||
|
|
||||||
internal static BatchSpec ClipPaletted { get; } = new(
|
internal static BatchSpec ClipPaletted { get; } = new(
|
||||||
RetailAlphaMeshRouter.MaskClipMap,
|
SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
|
||||||
IsTransparent: true,
|
Paletted: true, PositiveStippling: false);
|
||||||
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
|
||||||
PaletteId: 0x04000001u);
|
|
||||||
|
|
||||||
internal static BatchSpec ClipPositiveStippleDds { get; } = new(
|
internal static BatchSpec ClipPositiveStippleDds { get; } = new(
|
||||||
RetailAlphaMeshRouter.MaskClipMap | RetailAlphaMeshRouter.MaskPositiveStipple,
|
SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
|
||||||
IsTransparent: true,
|
Paletted: false, PositiveStippling: true);
|
||||||
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
|
|
||||||
PaletteId: 0);
|
|
||||||
|
|
||||||
internal static BatchSpec Alpha { get; } = new(
|
internal static BatchSpec Alpha { get; } = new(
|
||||||
RetailAlphaMeshRouter.MaskAlphaFamily,
|
SurfaceType.Base1Image | SurfaceType.Alpha,
|
||||||
IsTransparent: true,
|
Paletted: false, PositiveStippling: false);
|
||||||
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
|
||||||
PaletteId: 0);
|
|
||||||
|
|
||||||
internal static BatchSpec Additive { get; } = new(
|
internal static BatchSpec Additive { get; } = new(
|
||||||
RetailAlphaMeshRouter.MaskAlphaFamily,
|
SurfaceType.Base1Image | SurfaceType.Additive,
|
||||||
IsTransparent: true,
|
Paletted: false, PositiveStippling: false);
|
||||||
AcDream.Core.Meshing.TranslucencyKind.Additive,
|
|
||||||
PaletteId: 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -512,27 +638,45 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
for (int i = 0; i < specs.Length; i++)
|
for (int i = 0; i < specs.Length; i++)
|
||||||
{
|
{
|
||||||
BatchSpec spec = specs[i];
|
BatchSpec spec = specs[i];
|
||||||
|
TranslucencyKind translucency =
|
||||||
|
TranslucencyKindExtensions.FromSurfaceType(spec.Type);
|
||||||
|
bool alphaFamily = (spec.Type
|
||||||
|
& (SurfaceType.Alpha | SurfaceType.InvAlpha | SurfaceType.Additive)) != 0;
|
||||||
|
byte mask = RetailAlphaMeshRouter.ConstructSubsetMask(
|
||||||
|
alphaFamily,
|
||||||
|
(spec.Type & SurfaceType.Base1ClipMap) != 0,
|
||||||
|
(spec.Type & SurfaceType.Translucent) != 0,
|
||||||
|
spec.PositiveStippling);
|
||||||
|
uint paletteId = spec.Paletted ? 0x04000001u : 0u;
|
||||||
batches.Add(new TextureBatchData
|
batches.Add(new TextureBatchData
|
||||||
{
|
{
|
||||||
Key = new TextureKey
|
Key = new TextureKey
|
||||||
{
|
{
|
||||||
SurfaceId = 0x08000BFFu + (uint)i,
|
SurfaceId = 0x08000BFFu + (uint)i,
|
||||||
PaletteId = spec.PaletteId,
|
PaletteId = paletteId,
|
||||||
},
|
},
|
||||||
TextureData = new byte[8 * 8 * 4],
|
TextureData = new byte[8 * 8 * 4],
|
||||||
Indices = [0, 1, 2],
|
Indices = [0, 1, 2],
|
||||||
IsTransparent = spec.IsTransparent,
|
IsTransparent = translucency != TranslucencyKind.Opaque,
|
||||||
IsAdditive = spec.Translucency == AcDream.Core.Meshing.TranslucencyKind.Additive,
|
IsAdditive = (spec.Type & SurfaceType.Additive) != 0,
|
||||||
Translucency = spec.Translucency,
|
Translucency = translucency,
|
||||||
|
MaterialState = RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
spec.Type,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: spec.Paletted),
|
||||||
SurfaceOpacity = 0.25f,
|
SurfaceOpacity = 0.25f,
|
||||||
RetailSurfaceMask = spec.Mask,
|
RetailSurfaceMask = mask,
|
||||||
CullMode = CullMode.Clockwise,
|
CullMode = CullMode.Clockwise,
|
||||||
IsCellShell = true,
|
IsCellShell = true,
|
||||||
SourceSurfaceIndex = i,
|
SourceSurfaceIndex = i,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
mesh.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches;
|
mesh.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches;
|
||||||
Assert.NotNull(_meshManager.UploadMeshData(mesh));
|
ObjectRenderData uploaded = Assert.IsType<ObjectRenderData>(
|
||||||
|
_meshManager.UploadMeshData(mesh));
|
||||||
|
Assert.Equal(
|
||||||
|
batches.Select(static batch => batch.MaterialState),
|
||||||
|
uploaded.Batches.Select(static batch => batch.MaterialState));
|
||||||
|
|
||||||
Renderer = new EnvCellRenderer(
|
Renderer = new EnvCellRenderer(
|
||||||
Device,
|
Device,
|
||||||
|
|
|
||||||
|
|
@ -80,12 +80,14 @@ public sealed class GpuContractTests
|
||||||
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
|
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
|
||||||
// detail is now computed inside those same pipelines. Collapsing any of
|
// detail is now computed inside those same pipelines. Collapsing any of
|
||||||
// these modes changes the corresponding subset's composition.
|
// these modes changes the corresponding subset's composition.
|
||||||
Assert.Equal(5, Enum.GetValues<GpuBlendMode>().Length);
|
Assert.Equal(7, Enum.GetValues<GpuBlendMode>().Length);
|
||||||
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
||||||
|
Assert.Contains(GpuBlendMode.RawAdditive, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
|
Assert.Contains(GpuBlendMode.InverseAdditive, Enum.GetValues<GpuBlendMode>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,9 @@ public sealed class VulkanShaderManifestTests
|
||||||
// single-pass building detail material now runs inside the base
|
// single-pass building detail material now runs inside the base
|
||||||
// fragment. Final alpha is authoredOpacity*liveFade*detail.a^2;
|
// fragment. Final alpha is authoredOpacity*liveFade*detail.a^2;
|
||||||
// CLIP tests that value while detail-off retains base-alpha logic.
|
// CLIP tests that value while detail-off retains base-alpha logic.
|
||||||
["mesh_modern.frag.spv"] = "576af229b96a2a5e9b3b1ff16ab686d96578c2d9d1762b0e225df31bfd7aa86f",
|
// Fix round 1 additionally carries the exact SetSurface alpha-test
|
||||||
|
// reference and raw-additive fog suppression into this same pass.
|
||||||
|
["mesh_modern.frag.spv"] = "bd47fe8a33e0f1d025fe48fd95b034ba6fe591a6d20e88e636c86238d8773db1",
|
||||||
// S5-c4 replaces the padding word at batch offset 4 with authored
|
// S5-c4 replaces the padding word at batch offset 4 with authored
|
||||||
// surface opacity and forwards it plus exact detail category/flags
|
// surface opacity and forwards it plus exact detail category/flags
|
||||||
// flat to the fragment shader. The 16-byte ABI is unchanged.
|
// flat to the fragment shader. The 16-byte ABI is unchanged.
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,12 @@ public sealed class VulkanViewportMappingTests
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
(BlendFactor.SrcAlpha, BlendFactor.One),
|
(BlendFactor.SrcAlpha, BlendFactor.One),
|
||||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive));
|
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive));
|
||||||
|
Assert.Equal(
|
||||||
|
(BlendFactor.One, BlendFactor.One),
|
||||||
|
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RawAdditive));
|
||||||
|
Assert.Equal(
|
||||||
|
(BlendFactor.OneMinusSrcAlpha, BlendFactor.One),
|
||||||
|
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAdditive));
|
||||||
// Retail's third mode, found at slice V4c. Mapping it onto straight
|
// Retail's third mode, found at slice V4c. Mapping it onto straight
|
||||||
// alpha would have silently changed how every inverse-alpha surface
|
// alpha would have silently changed how every inverse-alpha surface
|
||||||
// composites.
|
// composites.
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,45 @@ public sealed class RetailDetailTextureContractTests
|
||||||
private static readonly Vector4 Detail = new(0.91f, 0.23f, 0.49f, 0.62f);
|
private static readonly Vector4 Detail = new(0.91f, 0.23f, 0.49f, 0.62f);
|
||||||
private static readonly Vector4 Destination = new(0.17f, 0.37f, 0.71f, 0.29f);
|
private static readonly Vector4 Destination = new(0.17f, 0.37f, 0.71f, 0.29f);
|
||||||
|
|
||||||
|
public static TheoryData<SurfaceType, bool, RetailSetSurfaceBlend,
|
||||||
|
RetailSetSurfaceAlphaTest, bool> ResolvedRows() => new()
|
||||||
|
{
|
||||||
|
{ SurfaceType.Base1Image, false, RetailSetSurfaceBlend.Opaque, RetailSetSurfaceAlphaTest.Disabled, true },
|
||||||
|
{ SurfaceType.Alpha, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
|
||||||
|
{ SurfaceType.Alpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.AlphaAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
{ SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
{ SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
|
||||||
|
{ SurfaceType.InvAlpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
{ SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Dds, true },
|
||||||
|
{ SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Paletted, true },
|
||||||
|
{ SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Paletted, true },
|
||||||
|
{ SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Dds, false },
|
||||||
|
{ SurfaceType.Translucent, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
|
||||||
|
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(ResolvedRows))]
|
||||||
|
public void SetSurfaceResolver_PreservesEveryRawFamilyClipAndLateOverride(
|
||||||
|
SurfaceType type,
|
||||||
|
bool paletted,
|
||||||
|
RetailSetSurfaceBlend expectedBlend,
|
||||||
|
RetailSetSurfaceAlphaTest expectedAlphaTest,
|
||||||
|
bool expectedFog)
|
||||||
|
{
|
||||||
|
RetailSetSurfaceMaterialState state = RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
type,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: paletted);
|
||||||
|
|
||||||
|
Assert.Equal(expectedBlend, state.Blend);
|
||||||
|
Assert.Equal(expectedAlphaTest, state.AlphaTest);
|
||||||
|
Assert.Equal(expectedFog, state.FogEnabled);
|
||||||
|
Assert.Equal(state, RetailSetSurfaceMaterialState.FromPackedByte(state.ToPackedByte()));
|
||||||
|
}
|
||||||
|
|
||||||
public static TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float> PolicyRows()
|
public static TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float> PolicyRows()
|
||||||
{
|
{
|
||||||
var rows = new TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float>();
|
var rows = new TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float>();
|
||||||
|
|
@ -65,27 +104,22 @@ public sealed class RetailDetailTextureContractTests
|
||||||
Assert.Equal(["source-subset"], PhysicalDrawSequence(detailEnabled));
|
Assert.Equal(["source-subset"], PhysicalDrawSequence(detailEnabled));
|
||||||
|
|
||||||
PipelineState state = ExpectedPipelineState(
|
PipelineState state = ExpectedPipelineState(
|
||||||
consumer, expectedKind, paletted, expectedReference);
|
consumer, surfaceType, expectedKind, paletted, detailEnabled);
|
||||||
Assert.Equal(expectedReference, state.AlphaReference);
|
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
|
||||||
Assert.Equal(
|
surfaceType,
|
||||||
expectedKind == TranslucencyKind.ClipMap
|
texturePresent: true,
|
||||||
? paletted ? 100f / 255f : 200f / 255f
|
textureHasPalette: paletted);
|
||||||
: 0.05f,
|
if (detailEnabled)
|
||||||
state.AlphaReference);
|
|
||||||
Assert.True(state.AlphaTest);
|
|
||||||
Assert.Equal(expectedKind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
|
|
||||||
state.DepthWrite);
|
|
||||||
Assert.Equal(expectedKind switch
|
|
||||||
{
|
{
|
||||||
TranslucencyKind.Opaque => GpuBlendMode.None,
|
Assert.Equal(resolved.AlphaTestReference, state.AlphaReference);
|
||||||
TranslucencyKind.AlphaBlend => GpuBlendMode.StraightAlpha,
|
Assert.Equal(resolved.AlphaTestEnabled, state.AlphaTest);
|
||||||
TranslucencyKind.Additive => GpuBlendMode.Additive,
|
Assert.Equal(resolved.FogEnabled, state.FogEnabled);
|
||||||
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha,
|
}
|
||||||
TranslucencyKind.ClipMap when consumer == "EnvCell" =>
|
else
|
||||||
GpuBlendMode.PremultipliedAlpha,
|
{
|
||||||
TranslucencyKind.ClipMap => GpuBlendMode.None,
|
Assert.Equal(expectedReference, state.AlphaReference);
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(expectedKind)),
|
Assert.True(state.AlphaTest);
|
||||||
}, state.Blend);
|
}
|
||||||
Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled));
|
Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled));
|
||||||
if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap)
|
if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap)
|
||||||
Assert.Equal("AP-240 immediate A2C", state.Placement);
|
Assert.Equal("AP-240 immediate A2C", state.Placement);
|
||||||
|
|
@ -263,11 +297,38 @@ public sealed class RetailDetailTextureContractTests
|
||||||
|
|
||||||
private static PipelineState ExpectedPipelineState(
|
private static PipelineState ExpectedPipelineState(
|
||||||
string consumer,
|
string consumer,
|
||||||
|
SurfaceType surfaceType,
|
||||||
TranslucencyKind kind,
|
TranslucencyKind kind,
|
||||||
bool paletted,
|
bool paletted,
|
||||||
float reference)
|
bool detailEnabled)
|
||||||
{
|
{
|
||||||
_ = paletted;
|
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
surfaceType,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: paletted);
|
||||||
|
if (detailEnabled)
|
||||||
|
{
|
||||||
|
GpuBlendMode exactBlend = resolved.Blend switch
|
||||||
|
{
|
||||||
|
RetailSetSurfaceBlend.Opaque => GpuBlendMode.None,
|
||||||
|
RetailSetSurfaceBlend.StraightAlpha => GpuBlendMode.StraightAlpha,
|
||||||
|
RetailSetSurfaceBlend.AlphaAdditive => GpuBlendMode.Additive,
|
||||||
|
RetailSetSurfaceBlend.Additive => GpuBlendMode.RawAdditive,
|
||||||
|
RetailSetSurfaceBlend.InverseAlpha => GpuBlendMode.InverseAlpha,
|
||||||
|
RetailSetSurfaceBlend.InverseAdditive => GpuBlendMode.InverseAdditive,
|
||||||
|
RetailSetSurfaceBlend.Clip => GpuBlendMode.PremultipliedAlpha,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(),
|
||||||
|
};
|
||||||
|
return new PipelineState(
|
||||||
|
exactBlend,
|
||||||
|
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
|
||||||
|
resolved.AlphaTestEnabled,
|
||||||
|
resolved.AlphaTestReference,
|
||||||
|
resolved.FogEnabled,
|
||||||
|
consumer == "Building" && kind == TranslucencyKind.ClipMap
|
||||||
|
? "AP-240 immediate A2C"
|
||||||
|
: "source FIFO position");
|
||||||
|
}
|
||||||
GpuBlendMode blend = kind switch
|
GpuBlendMode blend = kind switch
|
||||||
{
|
{
|
||||||
TranslucencyKind.Opaque => GpuBlendMode.None,
|
TranslucencyKind.Opaque => GpuBlendMode.None,
|
||||||
|
|
@ -283,7 +344,10 @@ public sealed class RetailDetailTextureContractTests
|
||||||
blend,
|
blend,
|
||||||
kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
|
kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
|
||||||
AlphaTest: true,
|
AlphaTest: true,
|
||||||
reference,
|
kind == TranslucencyKind.ClipMap
|
||||||
|
? paletted ? 100f / 255f : 200f / 255f
|
||||||
|
: 0.05f,
|
||||||
|
FogEnabled: true,
|
||||||
consumer == "Building" && kind == TranslucencyKind.ClipMap
|
consumer == "Building" && kind == TranslucencyKind.ClipMap
|
||||||
? "AP-240 immediate A2C"
|
? "AP-240 immediate A2C"
|
||||||
: "source FIFO position");
|
: "source FIFO position");
|
||||||
|
|
@ -310,6 +374,7 @@ public sealed class RetailDetailTextureContractTests
|
||||||
bool DepthWrite,
|
bool DepthWrite,
|
||||||
bool AlphaTest,
|
bool AlphaTest,
|
||||||
float AlphaReference,
|
float AlphaReference,
|
||||||
|
bool FogEnabled,
|
||||||
string Placement);
|
string Placement);
|
||||||
|
|
||||||
private static Vector4 IndependentComposite(
|
private static Vector4 IndependentComposite(
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
WalkDrawStage stage = WalkDrawStage.Terrain,
|
WalkDrawStage stage = WalkDrawStage.Terrain,
|
||||||
TranslucencyKind translucency = TranslucencyKind.Opaque,
|
TranslucencyKind translucency = TranslucencyKind.Opaque,
|
||||||
CullMode cullMode = CullMode.CounterClockwise,
|
CullMode cullMode = CullMode.CounterClockwise,
|
||||||
uint detailCategory = 0) =>
|
uint detailCategory = 0,
|
||||||
|
RetailSetSurfaceMaterialState? materialState = null) =>
|
||||||
new(
|
new(
|
||||||
Key: new GroupKey(
|
Key: new GroupKey(
|
||||||
FirstIndex: (uint)index * 3,
|
FirstIndex: (uint)index * 3,
|
||||||
|
|
@ -41,6 +42,7 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
TextureSlot: new GpuTextureSlot((uint)index),
|
TextureSlot: new GpuTextureSlot((uint)index),
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: translucency,
|
Translucency: translucency,
|
||||||
|
MaterialState: materialState ?? RetailSetSurfaceMaterialState.Opaque,
|
||||||
FoliageFlags: 0,
|
FoliageFlags: 0,
|
||||||
CullMode: cullMode),
|
CullMode: cullMode),
|
||||||
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),
|
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),
|
||||||
|
|
@ -284,8 +286,12 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
|
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace()
|
[InlineData(false, 200f / 255f)]
|
||||||
|
[InlineData(true, 100f / 255f)]
|
||||||
|
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace(
|
||||||
|
bool paletted,
|
||||||
|
float expectedReference)
|
||||||
{
|
{
|
||||||
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
|
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
|
||||||
using DrawScope draw = fx.BeginDraw();
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
|
@ -293,7 +299,14 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
OrderedDrawStream stream = StreamOf(
|
OrderedDrawStream stream = StreamOf(
|
||||||
MakeCommand(0),
|
MakeCommand(0),
|
||||||
MakeCommand(1, detailCategory: 1),
|
MakeCommand(1, detailCategory: 1),
|
||||||
MakeCommand(2, translucency: TranslucencyKind.ClipMap, detailCategory: 1),
|
MakeCommand(
|
||||||
|
2,
|
||||||
|
translucency: TranslucencyKind.ClipMap,
|
||||||
|
detailCategory: 1,
|
||||||
|
materialState: RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
SurfaceType.Base1ClipMap,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: paletted)),
|
||||||
MakeCommand(3));
|
MakeCommand(3));
|
||||||
|
|
||||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||||
|
|
@ -317,8 +330,90 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
.Select(call => call.PipelineName)
|
.Select(call => call.PipelineName)
|
||||||
.ToArray());
|
.ToArray());
|
||||||
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||||
|
Assert.Equal(expectedReference, runs[2].Constants.ParamB);
|
||||||
|
GpuPipelineDescription a2c = fx.Device.CreatedPipelines
|
||||||
|
.Single(pipeline => pipeline.Description.Name == "wb-mesh-opaque-a2c-1x")
|
||||||
|
.Description;
|
||||||
|
Assert.Equal(GpuBlendMode.None, a2c.Blend);
|
||||||
|
Assert.True(a2c.AlphaToCoverage);
|
||||||
|
Assert.True(a2c.Depth.Test);
|
||||||
|
Assert.True(a2c.Depth.Write);
|
||||||
|
Assert.Equal(WorldDepthContract.WorldCompare, a2c.Depth.Compare);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PrepareThenDraw_PureClipBuildingDetailOffRetainsAp240A2cAndNeutralDetailState()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: false);
|
||||||
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
OrderedDrawStream stream = StreamOf(
|
||||||
|
MakeCommand(0),
|
||||||
|
MakeCommand(
|
||||||
|
1,
|
||||||
|
translucency: TranslucencyKind.ClipMap,
|
||||||
|
detailCategory: 1,
|
||||||
|
materialState: RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
SurfaceType.Base1ClipMap,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: true)),
|
||||||
|
MakeCommand(2));
|
||||||
|
|
||||||
|
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||||
|
|
||||||
|
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
|
||||||
|
Assert.Equal([(0, 1), (1, 1), (2, 1)],
|
||||||
|
runs.Select(run => (run.Start, run.Count)).ToList());
|
||||||
|
Assert.Equal(0u, runs[1].Constants.TextureIndexA);
|
||||||
|
Assert.Equal(0f, runs[1].Constants.ParamA);
|
||||||
|
Assert.Equal(0f, runs[1].Constants.ParamB);
|
||||||
|
Assert.Equal(0, runs[1].Constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
|
||||||
|
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
|
call => call.PipelineName == "wb-mesh-opaque-a2c-1x");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AtmosphericReceiver_AdjacentOrdinaryDetailOrdinaryCommandsDoNotLeakExactState()
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture(
|
||||||
|
detailAvailable: true,
|
||||||
|
detailEnabled: true,
|
||||||
|
atmospheric: true);
|
||||||
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
OrderedDrawStream stream = StreamOf(
|
||||||
|
MakeCommand(0),
|
||||||
|
MakeCommand(
|
||||||
|
1,
|
||||||
|
translucency: TranslucencyKind.Additive,
|
||||||
|
detailCategory: 1,
|
||||||
|
materialState: RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
SurfaceType.Additive,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: false)),
|
||||||
|
MakeCommand(2));
|
||||||
|
|
||||||
|
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||||
|
|
||||||
|
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
|
||||||
|
Assert.Equal([(0, 1), (1, 1), (2, 1)],
|
||||||
|
runs.Select(run => (run.Start, run.Count)).ToList());
|
||||||
|
Assert.Equal((0u, 0f, 0f, 0), DetailState(runs[0].Constants));
|
||||||
|
Assert.Equal((77u, 3.5f, 0f, RetailDetailTextureContract.NoFogRenderPassFlag),
|
||||||
|
DetailState(runs[1].Constants));
|
||||||
|
Assert.Equal((0u, 0f, 0f, 0), DetailState(runs[2].Constants));
|
||||||
|
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
|
call => call.PipelineName == "wb-mesh-atmospheric-raw-additive-1x");
|
||||||
|
GpuPipelineDescription selected = fx.Device.CreatedPipelines
|
||||||
|
.Single(pipeline => pipeline.Description.Name == "wb-mesh-atmospheric-raw-additive-1x")
|
||||||
|
.Description;
|
||||||
|
Assert.Equal("mesh_atmospheric", selected.Shaders.Name);
|
||||||
|
Assert.Equal(GpuBlendMode.RawAdditive, selected.Blend);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (uint Slot, float Tiling, float Reference, int NoFog) DetailState(
|
||||||
|
GpuPushConstants constants) =>
|
||||||
|
(constants.TextureIndexA, constants.ParamA, constants.ParamB,
|
||||||
|
constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne()
|
public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne()
|
||||||
{
|
{
|
||||||
|
|
@ -609,7 +704,10 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
private readonly WbMeshAdapter _meshAdapter;
|
private readonly WbMeshAdapter _meshAdapter;
|
||||||
private readonly TextureCache _textures;
|
private readonly TextureCache _textures;
|
||||||
|
|
||||||
public DispatcherFixture(bool detailAvailable = false, bool detailEnabled = false)
|
public DispatcherFixture(
|
||||||
|
bool detailAvailable = false,
|
||||||
|
bool detailEnabled = false,
|
||||||
|
bool atmospheric = false)
|
||||||
{
|
{
|
||||||
Device = new RecordingGpuDevice();
|
Device = new RecordingGpuDevice();
|
||||||
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
||||||
|
|
@ -640,6 +738,15 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
|
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
|
||||||
: default,
|
: default,
|
||||||
buildingDetailEnabled: () => detailEnabled);
|
buildingDetailEnabled: () => detailEnabled);
|
||||||
|
if (atmospheric)
|
||||||
|
{
|
||||||
|
var source = new BindableAtmosphericSource();
|
||||||
|
WbDrawDispatcher.DirectionalShadowReceiverPipelineState candidate =
|
||||||
|
Assert.IsType<WbDrawDispatcher.DirectionalShadowReceiverPipelineState>(
|
||||||
|
Dispatcher.PrepareDirectionalShadowReceiver(source, sampleCount: 1));
|
||||||
|
Assert.Null(Dispatcher.SwapDirectionalShadowReceiver(candidate));
|
||||||
|
}
|
||||||
|
Atmospheric = atmospheric;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecordingGpuDevice Device { get; }
|
public RecordingGpuDevice Device { get; }
|
||||||
|
|
@ -650,6 +757,8 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
|
|
||||||
public WbDrawDispatcher Dispatcher { get; }
|
public WbDrawDispatcher Dispatcher { get; }
|
||||||
|
|
||||||
|
public bool Atmospheric { get; }
|
||||||
|
|
||||||
/// <summary>Opens a frame and a backbuffer pass, publishes it on
|
/// <summary>Opens a frame and a backbuffer pass, publishes it on
|
||||||
/// <see cref="Scope"/>, then clears the recorded calls so a test only
|
/// <see cref="Scope"/>, then clears the recorded calls so a test only
|
||||||
/// sees what its own <c>SubmitOrderedStream</c> call produced.</summary>
|
/// sees what its own <c>SubmitOrderedStream</c> call produced.</summary>
|
||||||
|
|
@ -659,7 +768,11 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
IGpuFrame frame = FrameLifetime.CurrentFrame!;
|
IGpuFrame frame = FrameLifetime.CurrentFrame!;
|
||||||
IGpuPassEncoder pass = frame.BeginPass(
|
IGpuPassEncoder pass = frame.BeginPass(
|
||||||
GpuPassDescription.BackbufferClear(
|
GpuPassDescription.BackbufferClear(
|
||||||
"fw2-ordered-stream-test", Vector4.Zero, sampleCount: 1));
|
Atmospheric
|
||||||
|
? DirectionalShadowReceiverPolicy.AtmosphericWorldPassName
|
||||||
|
: "fw2-ordered-stream-test",
|
||||||
|
Vector4.Zero,
|
||||||
|
sampleCount: 1));
|
||||||
IDisposable publication = Scope.Publish(pass);
|
IDisposable publication = Scope.Publish(pass);
|
||||||
Device.Clear();
|
Device.Clear();
|
||||||
return new DrawScope(frame, pass, publication);
|
return new DrawScope(frame, pass, publication);
|
||||||
|
|
@ -674,6 +787,30 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class BindableAtmosphericSource : IDirectionalShadowReceiverSource
|
||||||
|
{
|
||||||
|
public DirectionalShadowPipelineShaders PipelineShaders =>
|
||||||
|
DirectionalShadowPipelineShaders.Local;
|
||||||
|
|
||||||
|
public bool TryGetCurrentFrameBinding(
|
||||||
|
IGpuFrame frame,
|
||||||
|
out DirectionalShadowFrameBinding binding)
|
||||||
|
{
|
||||||
|
GpuRingAllocation allocation = frame.AllocateRing(
|
||||||
|
checked((int)DirectionalShadowUniforms.SizeInBytes),
|
||||||
|
GpuRingUsage.Uniform);
|
||||||
|
binding = new DirectionalShadowFrameBinding(
|
||||||
|
frame.Serial,
|
||||||
|
Enabled: false,
|
||||||
|
allocation.Buffer,
|
||||||
|
allocation.OffsetBytes,
|
||||||
|
DirectionalShadowUniforms.SizeInBytes,
|
||||||
|
GpuTextureSlot.Unassigned,
|
||||||
|
CascadeCount: 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||||
{
|
{
|
||||||
public PreparedAssetSourceStats Stats => default;
|
public PreparedAssetSourceStats Stats => default;
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ public sealed class OrderedDrawStreamTests
|
||||||
TextureSlot: new GpuTextureSlot((uint)index),
|
TextureSlot: new GpuTextureSlot((uint)index),
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: translucency,
|
Translucency: translucency,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
FoliageFlags: 0,
|
FoliageFlags: 0,
|
||||||
CullMode: cullMode),
|
CullMode: cullMode),
|
||||||
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),
|
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),
|
||||||
|
|
|
||||||
|
|
@ -105,11 +105,13 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
int indexCount,
|
int indexCount,
|
||||||
uint textureSlotIndex,
|
uint textureSlotIndex,
|
||||||
uint textureLayer = 0,
|
uint textureLayer = 0,
|
||||||
CullMode cullMode = CullMode.CounterClockwise) =>
|
CullMode cullMode = CullMode.CounterClockwise,
|
||||||
|
RetailSetSurfaceMaterialState? materialState = null) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
|
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
|
||||||
Translucency = translucency,
|
Translucency = translucency,
|
||||||
|
MaterialState = materialState ?? RetailSetSurfaceMaterialState.Opaque,
|
||||||
FirstIndex = firstIndex,
|
FirstIndex = firstIndex,
|
||||||
BaseVertex = (uint)baseVertex,
|
BaseVertex = (uint)baseVertex,
|
||||||
IndexCount = indexCount,
|
IndexCount = indexCount,
|
||||||
|
|
@ -316,14 +318,14 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
|
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
Assert.Contains("160 active rows", register, StringComparison.Ordinal);
|
Assert.Contains("active total to **161**", register, StringComparison.Ordinal);
|
||||||
int apSectionStart = register.IndexOf(
|
int apSectionStart = register.IndexOf(
|
||||||
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
|
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
|
||||||
int apSectionEnd = register.IndexOf(
|
int apSectionEnd = register.IndexOf(
|
||||||
"## 4. Temporary stopgap (TS)", StringComparison.Ordinal);
|
"## 4. Temporary stopgap (TS)", StringComparison.Ordinal);
|
||||||
Assert.True(apSectionStart >= 0, "AP section heading must exist.");
|
Assert.True(apSectionStart >= 0, "AP section heading must exist.");
|
||||||
Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section.");
|
Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section.");
|
||||||
foreach (string id in new[] { "AP-241", "AP-242", "AP-243" })
|
foreach (string id in new[] { "AP-241", "AP-242", "AP-243", "AP-244" })
|
||||||
{
|
{
|
||||||
int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal);
|
int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal);
|
||||||
Assert.True(
|
Assert.True(
|
||||||
|
|
@ -337,8 +339,10 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
line.StartsWith("| AP-242 |", StringComparison.Ordinal));
|
line.StartsWith("| AP-242 |", StringComparison.Ordinal));
|
||||||
Assert.Single(File.ReadLines(registerPath), static line =>
|
Assert.Single(File.ReadLines(registerPath), static line =>
|
||||||
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
|
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
|
||||||
|
Assert.Single(File.ReadLines(registerPath), static line =>
|
||||||
|
line.StartsWith("| AP-244 |", StringComparison.Ordinal));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
160,
|
161,
|
||||||
File.ReadLines(registerPath).Count(static line =>
|
File.ReadLines(registerPath).Count(static line =>
|
||||||
line.StartsWith("| AP-", StringComparison.Ordinal)));
|
line.StartsWith("| AP-", StringComparison.Ordinal)));
|
||||||
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
|
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
|
||||||
|
|
@ -417,7 +421,12 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh(
|
InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh(
|
||||||
MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1)));
|
MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1)));
|
||||||
InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh(
|
InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh(
|
||||||
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4, indexCount: 6, textureSlotIndex: 2)));
|
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4,
|
||||||
|
indexCount: 6, textureSlotIndex: 2,
|
||||||
|
materialState: RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
SurfaceType.Alpha | SurfaceType.Additive,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: false))));
|
||||||
|
|
||||||
var meshRefs = new[]
|
var meshRefs = new[]
|
||||||
{
|
{
|
||||||
|
|
@ -448,6 +457,7 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.Equal(3u, translucent.Key.FirstIndex);
|
Assert.Equal(3u, translucent.Key.FirstIndex);
|
||||||
Assert.Equal(6, translucent.Key.IndexCount);
|
Assert.Equal(6, translucent.Key.IndexCount);
|
||||||
Assert.Equal(2u, translucent.Key.TextureSlot.Index);
|
Assert.Equal(2u, translucent.Key.TextureSlot.Index);
|
||||||
|
Assert.Equal(RetailSetSurfaceBlend.AlphaAdditive, translucent.Key.MaterialState.Blend);
|
||||||
Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform);
|
Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform);
|
||||||
|
|
||||||
Assert.Equal(2, selectionParts.Count);
|
Assert.Equal(2, selectionParts.Count);
|
||||||
|
|
@ -1095,7 +1105,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
||||||
fx.AlphaQueue!.BeginFrame();
|
fx.AlphaQueue!.BeginFrame();
|
||||||
|
|
||||||
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
|
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0);
|
||||||
Vector3 localSortCenter = new(1, 2, 3);
|
Vector3 localSortCenter = new(1, 2, 3);
|
||||||
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
|
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
|
||||||
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
|
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
|
||||||
|
|
@ -1221,13 +1232,35 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(TranslucencyKind.AlphaBlend, "wb-mesh-alpha-1x")]
|
[InlineData(SurfaceType.Alpha, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)]
|
||||||
[InlineData(TranslucencyKind.Additive, "wb-mesh-additive-1x")]
|
[InlineData(SurfaceType.Alpha | SurfaceType.Additive, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 0f, false)]
|
||||||
[InlineData(TranslucencyKind.InvAlpha, "wb-mesh-inverse-1x")]
|
[InlineData(SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)]
|
||||||
public void ImmediateBuildingDetail_RetainsOriginalFramebufferFamily(
|
[InlineData(SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)]
|
||||||
TranslucencyKind kind,
|
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 0f, false)]
|
||||||
string expectedPipeline)
|
[InlineData(SurfaceType.Translucent, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)]
|
||||||
|
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 200f / 255f, true)]
|
||||||
|
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 100f / 255f, true)]
|
||||||
|
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 200f / 255f, false)]
|
||||||
|
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-additive-1x", GpuBlendMode.Additive, 100f / 255f, false)]
|
||||||
|
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 200f / 255f, false)]
|
||||||
|
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 100f / 255f, false)]
|
||||||
|
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 200f / 255f, true)]
|
||||||
|
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, true, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 100f / 255f, true)]
|
||||||
|
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 200f / 255f, false)]
|
||||||
|
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 100f / 255f, false)]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive,
|
||||||
|
true, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, false)]
|
||||||
|
public void ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState(
|
||||||
|
SurfaceType type,
|
||||||
|
bool paletted,
|
||||||
|
string expectedPipeline,
|
||||||
|
object expectedBlendValue,
|
||||||
|
float expectedReference,
|
||||||
|
bool expectedFog)
|
||||||
{
|
{
|
||||||
|
var expectedBlend = (GpuBlendMode)expectedBlendValue;
|
||||||
using var fx = new DispatcherFixture(
|
using var fx = new DispatcherFixture(
|
||||||
withAlphaQueue: true,
|
withAlphaQueue: true,
|
||||||
detailAvailable: true,
|
detailAvailable: true,
|
||||||
|
|
@ -1235,7 +1268,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
|
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
|
||||||
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
|
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
|
||||||
detailCategory: 1u,
|
detailCategory: 1u,
|
||||||
translucency: kind);
|
surfaceType: type,
|
||||||
|
paletted: paletted);
|
||||||
|
|
||||||
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
|
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
|
||||||
|
|
||||||
|
|
@ -1243,11 +1277,121 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
call => call.PipelineName == expectedPipeline);
|
call => call.PipelineName == expectedPipeline);
|
||||||
|
GpuPushConstants armed = fx.Device.Calls
|
||||||
|
.OfType<GpuRecordedPushConstants>()
|
||||||
|
.Last(call => call.Constants.ParamA != 0f)
|
||||||
|
.Constants;
|
||||||
|
Assert.Equal(99u, armed.TextureIndexA);
|
||||||
|
Assert.Equal(2f, armed.ParamA);
|
||||||
|
Assert.Equal(expectedReference, armed.ParamB);
|
||||||
|
Assert.Equal(!expectedFog,
|
||||||
|
(armed.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag) != 0);
|
||||||
|
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
type, texturePresent: true, textureHasPalette: paletted);
|
||||||
|
GpuPipelineDescription selected = fx.Device.CreatedPipelines
|
||||||
|
.Single(pipeline => pipeline.Description.Name == expectedPipeline)
|
||||||
|
.Description;
|
||||||
|
Assert.Equal(expectedBlend, selected.Blend);
|
||||||
|
Assert.True(selected.Depth.Test);
|
||||||
|
Assert.Equal(
|
||||||
|
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
|
||||||
|
selected.Depth.Write);
|
||||||
|
Assert.Equal(WorldDepthContract.WorldCompare, selected.Depth.Compare);
|
||||||
|
Vector4 source = RetailDetailTextureContract.Combine(
|
||||||
|
new Vector4(0.31f, 0.57f, 0.83f, 0.19f),
|
||||||
|
new Vector3(0.73f, 0.41f, 0.67f),
|
||||||
|
new Vector4(0.91f, 0.23f, 0.49f, 0.62f),
|
||||||
|
authoredOpacity: 0.75f,
|
||||||
|
liveOpacity: 0.4f);
|
||||||
|
Assert.Equal(0.3534682f, source.X, 6);
|
||||||
|
Assert.Equal(0.2330118f, source.Y, 6);
|
||||||
|
Assert.Equal(0.5438054f, source.Z, 6);
|
||||||
|
Assert.Equal(0.11532f, source.W, 6);
|
||||||
|
Vector4 destination = new(0.17f, 0.37f, 0.71f, 0.29f);
|
||||||
|
float x = source.W;
|
||||||
|
RetailDetailTextureContract.FramebufferFamily family = expectedBlend switch
|
||||||
|
{
|
||||||
|
GpuBlendMode.StraightAlpha => RetailDetailTextureContract.FramebufferFamily.Alpha,
|
||||||
|
GpuBlendMode.Additive => RetailDetailTextureContract.FramebufferFamily.AlphaAdditive,
|
||||||
|
GpuBlendMode.RawAdditive => RetailDetailTextureContract.FramebufferFamily.Additive,
|
||||||
|
GpuBlendMode.InverseAlpha => RetailDetailTextureContract.FramebufferFamily.InverseAlpha,
|
||||||
|
GpuBlendMode.InverseAdditive => RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(expectedBlend)),
|
||||||
|
};
|
||||||
|
Vector4 expectedFramebuffer = family switch
|
||||||
|
{
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Alpha => source * x + destination * (1f - x),
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive => source * x + destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Additive => source + destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.InverseAlpha => source * (1f - x) + destination * x,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive => source * (1f - x) + destination,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(family)),
|
||||||
|
};
|
||||||
|
Vector4 framebuffer = RetailDetailTextureContract.Composite(source, destination, family);
|
||||||
|
Assert.Equal(expectedFramebuffer.X, framebuffer.X, 6);
|
||||||
|
Assert.Equal(expectedFramebuffer.Y, framebuffer.Y, 6);
|
||||||
|
Assert.Equal(expectedFramebuffer.Z, framebuffer.Z, 6);
|
||||||
|
Assert.Equal(expectedFramebuffer.W, framebuffer.W, 6);
|
||||||
|
if (resolved.AlphaTestEnabled)
|
||||||
|
{
|
||||||
|
Assert.False(RetailDetailTextureContract.SurvivesClip(
|
||||||
|
MathF.BitDecrement(expectedReference), expectedReference));
|
||||||
|
Assert.True(RetailDetailTextureContract.SurvivesClip(expectedReference, expectedReference));
|
||||||
|
Assert.True(RetailDetailTextureContract.SurvivesClip(
|
||||||
|
MathF.BitIncrement(expectedReference), expectedReference));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(SurfaceType.Alpha, "wb-mesh-alpha-1x")]
|
||||||
|
[InlineData(SurfaceType.Alpha | SurfaceType.Additive, "wb-mesh-additive-1x")]
|
||||||
|
[InlineData(SurfaceType.Additive, "wb-mesh-additive-1x")]
|
||||||
|
[InlineData(SurfaceType.InvAlpha, "wb-mesh-inverse-1x")]
|
||||||
|
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive, "wb-mesh-additive-1x")]
|
||||||
|
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, "wb-mesh-alpha-1x")]
|
||||||
|
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
|
||||||
|
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
|
||||||
|
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, "wb-mesh-inverse-1x")]
|
||||||
|
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
|
||||||
|
[InlineData(SurfaceType.Translucent, "wb-mesh-alpha-1x")]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.Additive, "wb-mesh-additive-1x")]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha, "wb-mesh-inverse-1x")]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, "wb-mesh-alpha-1x")]
|
||||||
|
public void BuildingDetailOff_EveryRawStateRetainsThePreFixLogicalPath(
|
||||||
|
SurfaceType type,
|
||||||
|
string expectedPipeline)
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture(
|
||||||
|
withAlphaQueue: true,
|
||||||
|
detailAvailable: true,
|
||||||
|
detailEnabled: false);
|
||||||
|
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
|
||||||
|
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
|
||||||
|
detailCategory: 1u,
|
||||||
|
surfaceType: type);
|
||||||
|
|
||||||
|
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
|
||||||
|
|
||||||
|
Assert.Equal(1, fx.AlphaQueue!.PendingCount);
|
||||||
|
Assert.Empty(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
|
fx.AlphaQueue.EndFrame();
|
||||||
|
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
|
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
|
call => call.PipelineName == expectedPipeline);
|
||||||
|
GpuPushConstants constants = fx.Device.Calls
|
||||||
|
.OfType<GpuRecordedPushConstants>()
|
||||||
|
.Last()
|
||||||
|
.Constants;
|
||||||
|
Assert.Equal(0u, constants.TextureIndexA);
|
||||||
|
Assert.Equal(0f, constants.ParamA);
|
||||||
|
Assert.Equal(0f, constants.ParamB);
|
||||||
|
Assert.Equal(0, constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(
|
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(
|
||||||
uint detailCategory,
|
uint detailCategory,
|
||||||
TranslucencyKind translucency = TranslucencyKind.AlphaBlend) =>
|
SurfaceType surfaceType = SurfaceType.Alpha,
|
||||||
|
bool paletted = false) =>
|
||||||
new(
|
new(
|
||||||
new GroupKey(
|
new GroupKey(
|
||||||
0,
|
0,
|
||||||
|
|
@ -1255,7 +1399,11 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
3,
|
3,
|
||||||
new GpuTextureSlot(1),
|
new GpuTextureSlot(1),
|
||||||
0,
|
0,
|
||||||
translucency,
|
TranslucencyKindExtensions.FromSurfaceType(surfaceType),
|
||||||
|
RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
surfaceType,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: paletted),
|
||||||
FoliageFlags: 0),
|
FoliageFlags: 0),
|
||||||
Matrix4x4.Identity,
|
Matrix4x4.Identity,
|
||||||
ClipSlot: 0,
|
ClipSlot: 0,
|
||||||
|
|
@ -1277,6 +1425,10 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
TextureSlot = new GpuTextureSlot(1),
|
TextureSlot = new GpuTextureSlot(1),
|
||||||
TextureLayer = 0,
|
TextureLayer = 0,
|
||||||
Translucency = TranslucencyKind.AlphaBlend,
|
Translucency = TranslucencyKind.AlphaBlend,
|
||||||
|
MaterialState = RetailSetSurfaceMaterialState.Resolve(
|
||||||
|
SurfaceType.Alpha,
|
||||||
|
texturePresent: true,
|
||||||
|
textureHasPalette: false),
|
||||||
};
|
};
|
||||||
group.Matrices.Add(Matrix4x4.Identity);
|
group.Matrices.Add(Matrix4x4.Identity);
|
||||||
group.SubmissionOrders.Add(0);
|
group.SubmissionOrders.Add(0);
|
||||||
|
|
@ -1320,7 +1472,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
using var fx = new DispatcherFixture(withAlphaQueue: true);
|
||||||
fx.AlphaQueue!.BeginFrame();
|
fx.AlphaQueue!.BeginFrame();
|
||||||
|
|
||||||
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
|
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0);
|
||||||
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
|
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
|
||||||
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
|
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
|
||||||
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
|
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
|
||||||
|
|
@ -1354,7 +1507,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
|
|
||||||
var stream = new OrderedDrawStream();
|
var stream = new OrderedDrawStream();
|
||||||
stream.Append(new OrderedDrawCommand(
|
stream.Append(new OrderedDrawCommand(
|
||||||
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
|
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
|
||||||
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
|
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
|
||||||
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));
|
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ using AcDream.App.Rendering.Gpu.Vk;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Tests.Rendering.Gpu;
|
using AcDream.App.Tests.Rendering.Gpu;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using CullMode = DatReaderWriter.Enums.CullMode;
|
using CullMode = DatReaderWriter.Enums.CullMode;
|
||||||
|
|
@ -186,7 +187,8 @@ public class EnvCellRendererTests
|
||||||
"_mdiDrawRanges", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
"_mdiDrawRanges", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||||
var ranges = (List<EnvCellRenderer.MdiDrawRange>)rangesField.GetValue(renderer)!;
|
var ranges = (List<EnvCellRenderer.MdiDrawRange>)rangesField.GetValue(renderer)!;
|
||||||
ranges.Clear();
|
ranges.Clear();
|
||||||
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 0, firstCommand: 0, commandCount: 1);
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 0, firstCommand: 0, commandCount: 1,
|
||||||
|
RetailSetSurfaceMaterialState.Opaque);
|
||||||
|
|
||||||
var allInstances = new List<InstanceData>();
|
var allInstances = new List<InstanceData>();
|
||||||
for (int i = 0; i < instanceCount; i++)
|
for (int i = 0; i < instanceCount; i++)
|
||||||
|
|
@ -239,7 +241,7 @@ public class EnvCellRendererTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DetailUsesOnlyTheOriginalFourShellPipelines()
|
public void DetailUsesOnlySetSurfaceShellPipelinesAndNoReplayPipeline()
|
||||||
{
|
{
|
||||||
using var device = new RecordingGpuDevice();
|
using var device = new RecordingGpuDevice();
|
||||||
using var meshManager = CreateMeshManager(device);
|
using var meshManager = CreateMeshManager(device);
|
||||||
|
|
@ -250,7 +252,7 @@ public class EnvCellRendererTests
|
||||||
meshManager,
|
meshManager,
|
||||||
new WbFrustum());
|
new WbFrustum());
|
||||||
|
|
||||||
Assert.Equal(4, device.CreatedPipelines.Count);
|
Assert.Equal(7, device.CreatedPipelines.Count);
|
||||||
Assert.DoesNotContain(device.CreatedPipelines,
|
Assert.DoesNotContain(device.CreatedPipelines,
|
||||||
pipeline => pipeline.Description.Name.Contains("detail", StringComparison.Ordinal));
|
pipeline => pipeline.Description.Name.Contains("detail", StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
@ -260,11 +262,14 @@ public class EnvCellRendererTests
|
||||||
{
|
{
|
||||||
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
|
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
|
||||||
|
|
||||||
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3);
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3,
|
||||||
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 3, commandCount: 4);
|
RetailSetSurfaceMaterialState.Opaque);
|
||||||
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 3, commandCount: 4,
|
||||||
|
RetailSetSurfaceMaterialState.Opaque);
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[new EnvCellRenderer.MdiDrawRange(GroupIndex: 2, FirstCommand: 0, CommandCount: 7)],
|
[new EnvCellRenderer.MdiDrawRange(GroupIndex: 2, FirstCommand: 0, CommandCount: 7,
|
||||||
|
RetailSetSurfaceMaterialState.Opaque)],
|
||||||
ranges);
|
ranges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,17 +278,21 @@ public class EnvCellRendererTests
|
||||||
{
|
{
|
||||||
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
|
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
|
||||||
|
|
||||||
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3);
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3,
|
||||||
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 6, firstCommand: 3, commandCount: 2);
|
RetailSetSurfaceMaterialState.Opaque);
|
||||||
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 5, commandCount: 1);
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 6, firstCommand: 3, commandCount: 2,
|
||||||
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 9, commandCount: 2);
|
RetailSetSurfaceMaterialState.Opaque);
|
||||||
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 5, commandCount: 1,
|
||||||
|
RetailSetSurfaceMaterialState.Opaque);
|
||||||
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 9, commandCount: 2,
|
||||||
|
RetailSetSurfaceMaterialState.Opaque);
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[
|
[
|
||||||
new EnvCellRenderer.MdiDrawRange(2, 0, 3),
|
new EnvCellRenderer.MdiDrawRange(2, 0, 3, RetailSetSurfaceMaterialState.Opaque),
|
||||||
new EnvCellRenderer.MdiDrawRange(6, 3, 2),
|
new EnvCellRenderer.MdiDrawRange(6, 3, 2, RetailSetSurfaceMaterialState.Opaque),
|
||||||
new EnvCellRenderer.MdiDrawRange(2, 5, 1),
|
new EnvCellRenderer.MdiDrawRange(2, 5, 1, RetailSetSurfaceMaterialState.Opaque),
|
||||||
new EnvCellRenderer.MdiDrawRange(2, 9, 2),
|
new EnvCellRenderer.MdiDrawRange(2, 9, 2, RetailSetSurfaceMaterialState.Opaque),
|
||||||
],
|
],
|
||||||
ranges);
|
ranges);
|
||||||
}
|
}
|
||||||
|
|
@ -293,7 +302,8 @@ public class EnvCellRendererTests
|
||||||
{
|
{
|
||||||
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
|
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
|
||||||
|
|
||||||
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 0);
|
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 0,
|
||||||
|
RetailSetSurfaceMaterialState.Opaque);
|
||||||
|
|
||||||
Assert.Empty(ranges);
|
Assert.Empty(ranges);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ public sealed class FoliageWindClassificationTests
|
||||||
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(42),
|
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(42),
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: TranslucencyKind.ClipMap,
|
Translucency: TranslucencyKind.ClipMap,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise,
|
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise,
|
||||||
FoliageFlags: sceneryFlags);
|
FoliageFlags: sceneryFlags);
|
||||||
var landblockStaticKey = sceneryKey with { FoliageFlags = landblockStaticFlags };
|
var landblockStaticKey = sceneryKey with { FoliageFlags = landblockStaticFlags };
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,7 @@ public class InstanceGroupClearTests
|
||||||
TextureSlot: Slot(0xAA),
|
TextureSlot: Slot(0xAA),
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: TranslucencyKind.Opaque,
|
Translucency: TranslucencyKind.Opaque,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
FoliageFlags: 0u);
|
FoliageFlags: 0u);
|
||||||
var cached = new CachedBatch(
|
var cached = new CachedBatch(
|
||||||
key,
|
key,
|
||||||
|
|
@ -312,6 +313,7 @@ public class InstanceGroupClearTests
|
||||||
TextureSlot: Slot(0x77),
|
TextureSlot: Slot(0x77),
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: TranslucencyKind.ClipMap,
|
Translucency: TranslucencyKind.ClipMap,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
FoliageFlags: 0x2u,
|
FoliageFlags: 0x2u,
|
||||||
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise);
|
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise);
|
||||||
|
|
||||||
|
|
@ -416,6 +418,7 @@ public class InstanceGroupClearTests
|
||||||
TextureSlot: Slot(textureSlot),
|
TextureSlot: Slot(textureSlot),
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: TranslucencyKind.Opaque,
|
Translucency: TranslucencyKind.Opaque,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
FoliageFlags: 0u);
|
FoliageFlags: 0u);
|
||||||
|
|
||||||
private static WbDrawDispatcher.InstanceGroup MakeGroup(
|
private static WbDrawDispatcher.InstanceGroup MakeGroup(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
using Chorizite.Core.Render.Enums;
|
using Chorizite.Core.Render.Enums;
|
||||||
using DatReaderWriter;
|
using DatReaderWriter;
|
||||||
using DatReaderWriter.DBObjs;
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
@ -255,6 +256,60 @@ public sealed class MeshExtractorSolidFaceExtractionTests
|
||||||
BitConverter.SingleToInt32Bits(batch.SurfaceOpacity));
|
BitConverter.SingleToInt32Bits(batch.SurfaceOpacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static TheoryData<SurfaceType, bool, RetailSetSurfaceBlend,
|
||||||
|
RetailSetSurfaceAlphaTest, bool> SetSurfaceExtractionRows() => new()
|
||||||
|
{
|
||||||
|
{ SurfaceType.Base1Image, false, RetailSetSurfaceBlend.Opaque, RetailSetSurfaceAlphaTest.Disabled, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Alpha, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.AlphaAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Dds, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Paletted, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Paletted, true },
|
||||||
|
{ SurfaceType.Base1Image | SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, false },
|
||||||
|
};
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(SetSurfaceExtractionRows))]
|
||||||
|
public void OrdinaryAndCellExtraction_CarryIdenticalResolvedSetSurfaceState(
|
||||||
|
SurfaceType type,
|
||||||
|
bool paletted,
|
||||||
|
RetailSetSurfaceBlend expectedBlend,
|
||||||
|
RetailSetSurfaceAlphaTest expectedAlphaTest,
|
||||||
|
bool expectedFog)
|
||||||
|
{
|
||||||
|
var dats = new FakeMeshExtractorDats();
|
||||||
|
uint paletteId = paletted ? 0x04000001u : 0u;
|
||||||
|
RegisterTexturedQuad(
|
||||||
|
dats,
|
||||||
|
type,
|
||||||
|
PixelFormat.PFID_A8R8G8B8,
|
||||||
|
new byte[4 * 4 * 4],
|
||||||
|
paletteId: paletteId);
|
||||||
|
var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null);
|
||||||
|
|
||||||
|
ObjectMeshData ordinary = Assert.IsType<ObjectMeshData>(
|
||||||
|
extractor.PrepareMeshData(GfxObjId, isSetup: false));
|
||||||
|
ObjectMeshData cell = Assert.IsType<ObjectMeshData>(
|
||||||
|
extractor.PrepareCellStructMeshData(
|
||||||
|
id: 1,
|
||||||
|
BuildQuadCellStruct(RetailCullMode.Landblock),
|
||||||
|
surfaceOverrides: [2],
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
CancellationToken.None));
|
||||||
|
RetailSetSurfaceMaterialState ordinaryState =
|
||||||
|
Assert.Single(Assert.Single(ordinary.TextureBatches).Value).MaterialState;
|
||||||
|
RetailSetSurfaceMaterialState cellState =
|
||||||
|
Assert.Single(Assert.Single(cell.TextureBatches).Value).MaterialState;
|
||||||
|
|
||||||
|
Assert.Equal(ordinaryState, cellState);
|
||||||
|
Assert.Equal(expectedBlend, ordinaryState.Blend);
|
||||||
|
Assert.Equal(expectedAlphaTest, ordinaryState.AlphaTest);
|
||||||
|
Assert.Equal(expectedFog, ordinaryState.FogEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// OH2/S1 chunk-2 (2026-09-02): these two winding tests used to register
|
/// OH2/S1 chunk-2 (2026-09-02): these two winding tests used to register
|
||||||
/// an UNTEXTURED (Base1Solid) surface. Under the exact contract, an
|
/// an UNTEXTURED (Base1Solid) surface. Under the exact contract, an
|
||||||
|
|
@ -800,7 +855,8 @@ public sealed class MeshExtractorSolidFaceExtractionTests
|
||||||
SurfaceType surfaceType,
|
SurfaceType surfaceType,
|
||||||
PixelFormat pixelFormat,
|
PixelFormat pixelFormat,
|
||||||
byte[] sourceData,
|
byte[] sourceData,
|
||||||
float translucency = 0.0f)
|
float translucency = 0.0f,
|
||||||
|
uint paletteId = 0)
|
||||||
{
|
{
|
||||||
dats.RegisterRootGfxObj(GfxObjId, BuildQuadGfxObj(TexturedSurfaceId, noPos: false));
|
dats.RegisterRootGfxObj(GfxObjId, BuildQuadGfxObj(TexturedSurfaceId, noPos: false));
|
||||||
dats.Register(TexturedSurfaceId, new Surface
|
dats.Register(TexturedSurfaceId, new Surface
|
||||||
|
|
@ -818,6 +874,7 @@ public sealed class MeshExtractorSolidFaceExtractionTests
|
||||||
Width = 4,
|
Width = 4,
|
||||||
Height = 4,
|
Height = 4,
|
||||||
Format = pixelFormat,
|
Format = pixelFormat,
|
||||||
|
DefaultPaletteId = paletteId,
|
||||||
SourceData = sourceData,
|
SourceData = sourceData,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,8 @@ public static class ObjectMeshDataEquality {
|
||||||
Assert.True(expected.CullMode == actual.CullMode, $"{path}.CullMode: expected {expected.CullMode}, got {actual.CullMode}");
|
Assert.True(expected.CullMode == actual.CullMode, $"{path}.CullMode: expected {expected.CullMode}, got {actual.CullMode}");
|
||||||
Assert.True(expected.Translucency == actual.Translucency,
|
Assert.True(expected.Translucency == actual.Translucency,
|
||||||
$"{path}.Translucency: expected {expected.Translucency}, got {actual.Translucency}");
|
$"{path}.Translucency: expected {expected.Translucency}, got {actual.Translucency}");
|
||||||
|
Assert.True(expected.MaterialState == actual.MaterialState,
|
||||||
|
$"{path}.MaterialState: expected {expected.MaterialState}, got {actual.MaterialState}");
|
||||||
Assert.True(expected.IsTransparent == actual.IsTransparent, $"{path}.IsTransparent: expected {expected.IsTransparent}, got {actual.IsTransparent}");
|
Assert.True(expected.IsTransparent == actual.IsTransparent, $"{path}.IsTransparent: expected {expected.IsTransparent}, got {actual.IsTransparent}");
|
||||||
Assert.True(expected.IsAdditive == actual.IsAdditive, $"{path}.IsAdditive: expected {expected.IsAdditive}, got {actual.IsAdditive}");
|
Assert.True(expected.IsAdditive == actual.IsAdditive, $"{path}.IsAdditive: expected {expected.IsAdditive}, got {actual.IsAdditive}");
|
||||||
Assert.True(expected.HasWrappingUVs == actual.HasWrappingUVs, $"{path}.HasWrappingUVs: expected {expected.HasWrappingUVs}, got {actual.HasWrappingUVs}");
|
Assert.True(expected.HasWrappingUVs == actual.HasWrappingUVs, $"{path}.HasWrappingUVs: expected {expected.HasWrappingUVs}, got {actual.HasWrappingUVs}");
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,10 @@ public class ObjectMeshDataSerializerTests {
|
||||||
Indices = new List<ushort> { 0, 1, 2 },
|
Indices = new List<ushort> { 0, 1, 2 },
|
||||||
CullMode = CullMode.Clockwise,
|
CullMode = CullMode.Clockwise,
|
||||||
Translucency = TranslucencyKind.Opaque,
|
Translucency = TranslucencyKind.Opaque,
|
||||||
|
MaterialState = new RetailSetSurfaceMaterialState(
|
||||||
|
RetailSetSurfaceBlend.InverseAdditive,
|
||||||
|
RetailSetSurfaceAlphaTest.Paletted,
|
||||||
|
FogEnabled: false),
|
||||||
IsTransparent = false,
|
IsTransparent = false,
|
||||||
IsAdditive = false,
|
IsAdditive = false,
|
||||||
HasWrappingUVs = false,
|
HasWrappingUVs = false,
|
||||||
|
|
@ -290,6 +294,37 @@ public class ObjectMeshDataSerializerTests {
|
||||||
BitConverter.SingleToInt32Bits(read.SurfaceOpacity));
|
BitConverter.SingleToInt32Bits(read.SurfaceOpacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 0, true)]
|
||||||
|
[InlineData(1, 0, true)]
|
||||||
|
[InlineData(2, 1, true)]
|
||||||
|
[InlineData(3, 2, false)]
|
||||||
|
[InlineData(4, 0, true)]
|
||||||
|
[InlineData(5, 1, false)]
|
||||||
|
[InlineData(6, 2, true)]
|
||||||
|
public void SetSurfaceState_RoundTripsEveryPackedFieldDeterministically(
|
||||||
|
int blendValue,
|
||||||
|
int alphaTestValue,
|
||||||
|
bool fogEnabled) {
|
||||||
|
ObjectMeshData data = WithCellShellSubset();
|
||||||
|
TextureBatchData batch = Assert.Single(Assert.Single(data.TextureBatches).Value);
|
||||||
|
batch.MaterialState = new RetailSetSurfaceMaterialState(
|
||||||
|
(RetailSetSurfaceBlend)blendValue,
|
||||||
|
(RetailSetSurfaceAlphaTest)alphaTestValue,
|
||||||
|
fogEnabled);
|
||||||
|
|
||||||
|
using var first = new MemoryStream();
|
||||||
|
ObjectMeshDataSerializer.Write(data, first);
|
||||||
|
using var second = new MemoryStream();
|
||||||
|
ObjectMeshDataSerializer.Write(data, second);
|
||||||
|
|
||||||
|
Assert.Equal(first.ToArray(), second.ToArray());
|
||||||
|
TextureBatchData read = Assert.Single(Assert.Single(
|
||||||
|
ObjectMeshDataSerializer.Read(first.ToArray()).TextureBatches).Value);
|
||||||
|
Assert.Equal(batch.MaterialState, read.MaterialState);
|
||||||
|
Assert.Equal(batch.MaterialState.ToPackedByte(), read.MaterialState.ToPackedByte());
|
||||||
|
}
|
||||||
|
|
||||||
// ---- determinism -----------------------------------------------------
|
// ---- determinism -----------------------------------------------------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ public class PakFormatTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S5-c4: recipe 9 carries authored surface opacity in every prepared
|
/// S5-c4 fix round 1: recipe 10 carries exact SetSurface state after
|
||||||
/// texture batch. It remains a payload recipe change, not pak framing.
|
/// recipe 10's resolved SetSurface state after recipe 9's authored opacity.
|
||||||
|
/// It remains a payload recipe change.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatVersion_StaysAtTwo_AcrossTheS5C4RecipeBump() {
|
public void FormatVersion_StaysAtTwo_AcrossTheS5C4RecipeBump() {
|
||||||
Assert.Equal(2u, PakFormat.CurrentFormatVersion);
|
Assert.Equal(2u, PakFormat.CurrentFormatVersion);
|
||||||
Assert.Equal(9u, PakFormat.CurrentBakeToolVersion);
|
Assert.Equal(10u, PakFormat.CurrentBakeToolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S5-c4: an older recipe-8 package must be rejected by a recipe-9
|
/// S5-c4 fix round 1: an older recipe-9 package must be rejected by a recipe-10
|
||||||
/// consumer through the catalog-identity check
|
/// consumer through the catalog-identity check
|
||||||
/// alone, BEFORE any TextureBatchData payload field is ever
|
/// alone, BEFORE any TextureBatchData payload field is ever
|
||||||
/// deserialized -- so an older pak can never be silently misread as
|
/// deserialized -- so an older pak can never be silently misread as
|
||||||
|
|
@ -160,7 +160,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
|
||||||
fs.Position = 36;
|
fs.Position = 36;
|
||||||
Span<byte> buf = stackalloc byte[4];
|
Span<byte> buf = stackalloc byte[4];
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
|
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
|
||||||
buf, 8u);
|
buf, 9u);
|
||||||
fs.Write(buf);
|
fs.Write(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,7 +168,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
|
||||||
() => new PakPreparedAssetSource(path, Identity));
|
() => new PakPreparedAssetSource(path, Identity));
|
||||||
Assert.Contains("does not match the installed DAT set", error.Message);
|
Assert.Contains("does not match the installed DAT set", error.Message);
|
||||||
Assert.Contains("Re-bake", error.Message);
|
Assert.Contains("Re-bake", error.Message);
|
||||||
Assert.Contains("bake tool 8 != 9", error.Message);
|
Assert.Contains("bake tool 9 != 10", error.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,7 @@ public class EntityClassificationCacheTests
|
||||||
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
|
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: TranslucencyKind.Opaque,
|
Translucency: TranslucencyKind.Opaque,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
FoliageFlags: 0u);
|
FoliageFlags: 0u);
|
||||||
return new CachedBatch(key, new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), Matrix4x4.Identity);
|
return new CachedBatch(key, new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), Matrix4x4.Identity);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -426,6 +426,7 @@ public sealed class WbDrawDispatcherBucketingTests
|
||||||
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
|
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: TranslucencyKind.Opaque,
|
Translucency: TranslucencyKind.Opaque,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
FoliageFlags: 0u);
|
FoliageFlags: 0u);
|
||||||
return new CachedBatch(
|
return new CachedBatch(
|
||||||
key,
|
key,
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
||||||
// Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances)
|
// Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances)
|
||||||
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
||||||
{
|
{
|
||||||
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.75f),
|
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.75f),
|
||||||
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, SurfaceOpacity: 0.25f),
|
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.25f),
|
||||||
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.5f),
|
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.5f),
|
||||||
};
|
};
|
||||||
|
|
||||||
var indirect = new DrawElementsIndirectCommand[16];
|
var indirect = new DrawElementsIndirectCommand[16];
|
||||||
|
|
@ -75,12 +75,15 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
||||||
{
|
{
|
||||||
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0,
|
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0,
|
||||||
TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.Opaque,
|
TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.Opaque,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
CullMode: CullMode.Clockwise),
|
CullMode: CullMode.Clockwise),
|
||||||
new(IndexCount: 20, FirstIndex: 10, BaseVertex: 0, InstanceCount: 1, FirstInstance: 1,
|
new(IndexCount: 20, FirstIndex: 10, BaseVertex: 0, InstanceCount: 1, FirstInstance: 1,
|
||||||
TextureIndex: 0x2, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend,
|
TextureIndex: 0x2, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
CullMode: CullMode.None),
|
CullMode: CullMode.None),
|
||||||
new(IndexCount: 30, FirstIndex: 30, BaseVertex: 0, InstanceCount: 1, FirstInstance: 2,
|
new(IndexCount: 30, FirstIndex: 30, BaseVertex: 0, InstanceCount: 1, FirstInstance: 2,
|
||||||
TextureIndex: 0x3, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap,
|
TextureIndex: 0x3, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque,
|
||||||
CullMode: CullMode.Landblock),
|
CullMode: CullMode.Landblock),
|
||||||
};
|
};
|
||||||
var indirect = new DrawElementsIndirectCommand[4];
|
var indirect = new DrawElementsIndirectCommand[4];
|
||||||
|
|
@ -116,7 +119,7 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
||||||
// because the discard handles transparency, not blending.
|
// because the discard handles transparency, not blending.
|
||||||
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
||||||
{
|
{
|
||||||
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap),
|
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap, MaterialState: RetailSetSurfaceMaterialState.Opaque),
|
||||||
};
|
};
|
||||||
var indirect = new DrawElementsIndirectCommand[4];
|
var indirect = new DrawElementsIndirectCommand[4];
|
||||||
var batch = new WbDrawDispatcher.BatchDataPublic[4];
|
var batch = new WbDrawDispatcher.BatchDataPublic[4];
|
||||||
|
|
@ -146,7 +149,8 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
||||||
FirstInstance: index,
|
FirstInstance: index,
|
||||||
TextureIndex: (uint)index,
|
TextureIndex: (uint)index,
|
||||||
TextureLayer: 0,
|
TextureLayer: 0,
|
||||||
Translucency: kind)).ToList();
|
Translucency: kind,
|
||||||
|
MaterialState: RetailSetSurfaceMaterialState.Opaque)).ToList();
|
||||||
var indirect = new DrawElementsIndirectCommand[kinds.Length];
|
var indirect = new DrawElementsIndirectCommand[kinds.Length];
|
||||||
var batches = new WbDrawDispatcher.BatchDataPublic[kinds.Length];
|
var batches = new WbDrawDispatcher.BatchDataPublic[kinds.Length];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,4 +77,28 @@ public sealed class ContentMigrationCatalogTests
|
||||||
Assert.Contains("CellStruct", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("CellStruct", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecipeNineToTenRequiresOneExplicitFullRebuild()
|
||||||
|
{
|
||||||
|
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(9, 10);
|
||||||
|
|
||||||
|
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
|
||||||
|
Assert.Equal(9u, plan.FromRecipeVersion);
|
||||||
|
Assert.Equal(10u, plan.TargetRecipeVersion);
|
||||||
|
Assert.Contains("SetSurface", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Empty(plan.EffectiveDatIds);
|
||||||
|
Assert.Empty(plan.EffectiveLandblocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnyOlderRecipeToTenCollapsesToOneFullRebuild()
|
||||||
|
{
|
||||||
|
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(1, 10);
|
||||||
|
|
||||||
|
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
|
||||||
|
Assert.Equal(10u, plan.TargetRecipeVersion);
|
||||||
|
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains("SetSurface", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue