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

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

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

View file

@ -259,8 +259,22 @@ heading/velocity/parent state, arms no teleport hook, and still acknowledges
immediately. The long AP-148 row below is retained only as historical filing
research and is no longer active.
**S5-c4 fix-round-1 AP-232 correction (2026-09-05):** AP-244 below raises
the active total to **161**; the historical heading's 160 is superseded.
AP-232's retirement remains valid for the old two-draw/detail-weight
deviation, with this corrected scope: recipe 10 carries extraction-resolved
SetSurface blend, alpha-test/reference, and fog state through both real command
owners. Detail-active Wb and EnvCell select raw Additive, Alpha+Additive,
inverse, inverse-additive, combined/pure ClipMap, late Translucent override,
final-X reference, and raw-Additive fog-disable state exactly. Wb pure Clip
retains AP-240's immediate opaque/A2C pipeline while gaining final-X ParamB;
only EnvCell pure Clip uses `ONE/INVSRCALPHA`. The retired row's recipe-9-only
and `None` residual wording is superseded by this correction and AP-244;
AP-238/AP-239/AP-240 remain separate queue/token/raw-mask/placement residuals.
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
| AP-244 | **Filed 2026-09-05 at Campaign OVERHAUL S5-c4 fix round 1.** The new exact resolved SetSurface state is deliberately selected only when a Wb/EnvCell command has active building detail, preserving §23's bounded detail-off behavior. With detail off, Wb still collapses raw Additive and InvAlpha+Additive through `TranslucencyKind.Additive` to `SRCALPHA,ONE`; EnvCell additionally selects raw `IsAdditive` before the late `Translucent\|ClipMap` override and has no detail-off inverse pipeline. Both shader families retain the existing non-detail 0.05 discard rather than every combined ClipMap's palette/DDS reference, and fixed-function fog remains enabled for raw-Additive detail-off rows. AP-240 separately covers Wb pure-Clip placement/A2C. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`PipelineForBlend`, detail gates); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`ResolveBatchGroupIndex`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (detail-active exact selection versus retained range buckets); `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`; `src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag` | Fix round 1 was expressly bounded to detail-active one-pass material truth. The immutable state is already present end-to-end, so a later dedicated detail-off correction can select it without DAT lookup or new ownership, but must re-gate FIFO/routing/order and visual behavior together. | Detail-off raw Additive/inverse-additive brightness, inverse EnvCell compositing, combined ClipMap cutout threshold, late Translucent override, and raw-Additive fog can differ from retail. | `D3DPolyRender::SetSurface @0x0059C4D0` lines 425083425303; Campaign OVERHAUL S5-c4 §23.2 |
| AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001``ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` |
| AP-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 |

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -181,6 +181,13 @@ public class TextureBatchData {
/// </summary>
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>
/// Retail source surface-array index this subset was constructed from
/// (contract §3.6) — the CellStruct subset/material OWNER, not the

View file

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

View file

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

View file

@ -38,6 +38,135 @@ public enum TranslucencyKind
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
{
// Translucent override comes FIRST, then the existing priority chain:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,6 +14,45 @@ public sealed class RetailDetailTextureContractTests
private static readonly Vector4 Detail = new(0.91f, 0.23f, 0.49f, 0.62f);
private static readonly Vector4 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()
{
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));
PipelineState state = ExpectedPipelineState(
consumer, expectedKind, paletted, expectedReference);
Assert.Equal(expectedReference, state.AlphaReference);
Assert.Equal(
expectedKind == TranslucencyKind.ClipMap
? paletted ? 100f / 255f : 200f / 255f
: 0.05f,
state.AlphaReference);
Assert.True(state.AlphaTest);
Assert.Equal(expectedKind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
state.DepthWrite);
Assert.Equal(expectedKind switch
consumer, surfaceType, expectedKind, paletted, detailEnabled);
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
surfaceType,
texturePresent: true,
textureHasPalette: paletted);
if (detailEnabled)
{
TranslucencyKind.Opaque => GpuBlendMode.None,
TranslucencyKind.AlphaBlend => GpuBlendMode.StraightAlpha,
TranslucencyKind.Additive => GpuBlendMode.Additive,
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha,
TranslucencyKind.ClipMap when consumer == "EnvCell" =>
GpuBlendMode.PremultipliedAlpha,
TranslucencyKind.ClipMap => GpuBlendMode.None,
_ => throw new ArgumentOutOfRangeException(nameof(expectedKind)),
}, state.Blend);
Assert.Equal(resolved.AlphaTestReference, state.AlphaReference);
Assert.Equal(resolved.AlphaTestEnabled, state.AlphaTest);
Assert.Equal(resolved.FogEnabled, state.FogEnabled);
}
else
{
Assert.Equal(expectedReference, state.AlphaReference);
Assert.True(state.AlphaTest);
}
Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled));
if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap)
Assert.Equal("AP-240 immediate A2C", state.Placement);
@ -263,11 +297,38 @@ public sealed class RetailDetailTextureContractTests
private static PipelineState ExpectedPipelineState(
string consumer,
SurfaceType surfaceType,
TranslucencyKind kind,
bool paletted,
float reference)
bool detailEnabled)
{
_ = paletted;
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
surfaceType,
texturePresent: true,
textureHasPalette: paletted);
if (detailEnabled)
{
GpuBlendMode exactBlend = resolved.Blend switch
{
RetailSetSurfaceBlend.Opaque => GpuBlendMode.None,
RetailSetSurfaceBlend.StraightAlpha => GpuBlendMode.StraightAlpha,
RetailSetSurfaceBlend.AlphaAdditive => GpuBlendMode.Additive,
RetailSetSurfaceBlend.Additive => GpuBlendMode.RawAdditive,
RetailSetSurfaceBlend.InverseAlpha => GpuBlendMode.InverseAlpha,
RetailSetSurfaceBlend.InverseAdditive => GpuBlendMode.InverseAdditive,
RetailSetSurfaceBlend.Clip => GpuBlendMode.PremultipliedAlpha,
_ => throw new ArgumentOutOfRangeException(),
};
return new PipelineState(
exactBlend,
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
resolved.AlphaTestEnabled,
resolved.AlphaTestReference,
resolved.FogEnabled,
consumer == "Building" && kind == TranslucencyKind.ClipMap
? "AP-240 immediate A2C"
: "source FIFO position");
}
GpuBlendMode blend = kind switch
{
TranslucencyKind.Opaque => GpuBlendMode.None,
@ -283,7 +344,10 @@ public sealed class RetailDetailTextureContractTests
blend,
kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
AlphaTest: true,
reference,
kind == TranslucencyKind.ClipMap
? paletted ? 100f / 255f : 200f / 255f
: 0.05f,
FogEnabled: true,
consumer == "Building" && kind == TranslucencyKind.ClipMap
? "AP-240 immediate A2C"
: "source FIFO position");
@ -310,6 +374,7 @@ public sealed class RetailDetailTextureContractTests
bool DepthWrite,
bool AlphaTest,
float AlphaReference,
bool FogEnabled,
string Placement);
private static Vector4 IndependentComposite(

View file

@ -32,7 +32,8 @@ public sealed class OrderPreservingSubmitterTests
WalkDrawStage stage = WalkDrawStage.Terrain,
TranslucencyKind translucency = TranslucencyKind.Opaque,
CullMode cullMode = CullMode.CounterClockwise,
uint detailCategory = 0) =>
uint detailCategory = 0,
RetailSetSurfaceMaterialState? materialState = null) =>
new(
Key: new GroupKey(
FirstIndex: (uint)index * 3,
@ -41,6 +42,7 @@ public sealed class OrderPreservingSubmitterTests
TextureSlot: new GpuTextureSlot((uint)index),
TextureLayer: 0,
Translucency: translucency,
MaterialState: materialState ?? RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0,
CullMode: cullMode),
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),
@ -284,8 +286,12 @@ public sealed class OrderPreservingSubmitterTests
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
}
[Fact]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace()
[Theory]
[InlineData(false, 200f / 255f)]
[InlineData(true, 100f / 255f)]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace(
bool paletted,
float expectedReference)
{
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
using DrawScope draw = fx.BeginDraw();
@ -293,7 +299,14 @@ public sealed class OrderPreservingSubmitterTests
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(1, detailCategory: 1),
MakeCommand(2, translucency: TranslucencyKind.ClipMap, detailCategory: 1),
MakeCommand(
2,
translucency: TranslucencyKind.ClipMap,
detailCategory: 1,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Base1ClipMap,
texturePresent: true,
textureHasPalette: paletted)),
MakeCommand(3));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
@ -317,8 +330,90 @@ public sealed class OrderPreservingSubmitterTests
.Select(call => call.PipelineName)
.ToArray());
Assert.Equal(4, fx.Device.Calls.OfType<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]
public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne()
{
@ -609,7 +704,10 @@ public sealed class OrderPreservingSubmitterTests
private readonly WbMeshAdapter _meshAdapter;
private readonly TextureCache _textures;
public DispatcherFixture(bool detailAvailable = false, bool detailEnabled = false)
public DispatcherFixture(
bool detailAvailable = false,
bool detailEnabled = false,
bool atmospheric = false)
{
Device = new RecordingGpuDevice();
FrameLifetime = new GpuDeviceFrameLifetime(Device);
@ -640,6 +738,15 @@ public sealed class OrderPreservingSubmitterTests
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
: default,
buildingDetailEnabled: () => detailEnabled);
if (atmospheric)
{
var source = new BindableAtmosphericSource();
WbDrawDispatcher.DirectionalShadowReceiverPipelineState candidate =
Assert.IsType<WbDrawDispatcher.DirectionalShadowReceiverPipelineState>(
Dispatcher.PrepareDirectionalShadowReceiver(source, sampleCount: 1));
Assert.Null(Dispatcher.SwapDirectionalShadowReceiver(candidate));
}
Atmospheric = atmospheric;
}
public RecordingGpuDevice Device { get; }
@ -650,6 +757,8 @@ public sealed class OrderPreservingSubmitterTests
public WbDrawDispatcher Dispatcher { get; }
public bool Atmospheric { get; }
/// <summary>Opens a frame and a backbuffer pass, publishes it on
/// <see cref="Scope"/>, then clears the recorded calls so a test only
/// sees what its own <c>SubmitOrderedStream</c> call produced.</summary>
@ -659,7 +768,11 @@ public sealed class OrderPreservingSubmitterTests
IGpuFrame frame = FrameLifetime.CurrentFrame!;
IGpuPassEncoder pass = frame.BeginPass(
GpuPassDescription.BackbufferClear(
"fw2-ordered-stream-test", Vector4.Zero, sampleCount: 1));
Atmospheric
? DirectionalShadowReceiverPolicy.AtmosphericWorldPassName
: "fw2-ordered-stream-test",
Vector4.Zero,
sampleCount: 1));
IDisposable publication = Scope.Publish(pass);
Device.Clear();
return new DrawScope(frame, pass, publication);
@ -674,6 +787,30 @@ public sealed class OrderPreservingSubmitterTests
}
}
private sealed class BindableAtmosphericSource : IDirectionalShadowReceiverSource
{
public DirectionalShadowPipelineShaders PipelineShaders =>
DirectionalShadowPipelineShaders.Local;
public bool TryGetCurrentFrameBinding(
IGpuFrame frame,
out DirectionalShadowFrameBinding binding)
{
GpuRingAllocation allocation = frame.AllocateRing(
checked((int)DirectionalShadowUniforms.SizeInBytes),
GpuRingUsage.Uniform);
binding = new DirectionalShadowFrameBinding(
frame.Serial,
Enabled: false,
allocation.Buffer,
allocation.OffsetBytes,
DirectionalShadowUniforms.SizeInBytes,
GpuTextureSlot.Unassigned,
CascadeCount: 0);
return true;
}
}
private sealed class NullPreparedAssetSource : IPreparedAssetSource
{
public PreparedAssetSourceStats Stats => default;

View file

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

View file

@ -105,11 +105,13 @@ public sealed class WalkStaticStreamPopulatorTests
int indexCount,
uint textureSlotIndex,
uint textureLayer = 0,
CullMode cullMode = CullMode.CounterClockwise) =>
CullMode cullMode = CullMode.CounterClockwise,
RetailSetSurfaceMaterialState? materialState = null) =>
new()
{
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
Translucency = translucency,
MaterialState = materialState ?? RetailSetSurfaceMaterialState.Opaque,
FirstIndex = firstIndex,
BaseVertex = (uint)baseVertex,
IndexCount = indexCount,
@ -316,14 +318,14 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.DoesNotContain(
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("160 active rows", register, StringComparison.Ordinal);
Assert.Contains("active total to **161**", register, StringComparison.Ordinal);
int apSectionStart = register.IndexOf(
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
int apSectionEnd = register.IndexOf(
"## 4. Temporary stopgap (TS)", StringComparison.Ordinal);
Assert.True(apSectionStart >= 0, "AP section heading must exist.");
Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section.");
foreach (string id in new[] { "AP-241", "AP-242", "AP-243" })
foreach (string id in new[] { "AP-241", "AP-242", "AP-243", "AP-244" })
{
int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal);
Assert.True(
@ -337,8 +339,10 @@ public sealed class WalkStaticStreamPopulatorTests
line.StartsWith("| AP-242 |", StringComparison.Ordinal));
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-244 |", StringComparison.Ordinal));
Assert.Equal(
160,
161,
File.ReadLines(registerPath).Count(static line =>
line.StartsWith("| AP-", StringComparison.Ordinal)));
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
@ -417,7 +421,12 @@ public sealed class WalkStaticStreamPopulatorTests
InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh(
MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1)));
InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh(
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4, indexCount: 6, textureSlotIndex: 2)));
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4,
indexCount: 6, textureSlotIndex: 2,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Alpha | SurfaceType.Additive,
texturePresent: true,
textureHasPalette: false))));
var meshRefs = new[]
{
@ -448,6 +457,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Equal(3u, translucent.Key.FirstIndex);
Assert.Equal(6, translucent.Key.IndexCount);
Assert.Equal(2u, translucent.Key.TextureSlot.Index);
Assert.Equal(RetailSetSurfaceBlend.AlphaAdditive, translucent.Key.MaterialState.Blend);
Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform);
Assert.Equal(2, selectionParts.Count);
@ -1095,7 +1105,8 @@ public sealed class WalkStaticStreamPopulatorTests
using var fx = new DispatcherFixture(withAlphaQueue: true);
fx.AlphaQueue!.BeginFrame();
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0);
Vector3 localSortCenter = new(1, 2, 3);
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
@ -1221,13 +1232,35 @@ public sealed class WalkStaticStreamPopulatorTests
}
[Theory]
[InlineData(TranslucencyKind.AlphaBlend, "wb-mesh-alpha-1x")]
[InlineData(TranslucencyKind.Additive, "wb-mesh-additive-1x")]
[InlineData(TranslucencyKind.InvAlpha, "wb-mesh-inverse-1x")]
public void ImmediateBuildingDetail_RetainsOriginalFramebufferFamily(
TranslucencyKind kind,
string expectedPipeline)
[InlineData(SurfaceType.Alpha, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 0f, false)]
[InlineData(SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)]
[InlineData(SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 0f, false)]
[InlineData(SurfaceType.Translucent, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)]
[InlineData(SurfaceType.Translucent | SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 200f / 255f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 100f / 255f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 200f / 255f, false)]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-additive-1x", GpuBlendMode.Additive, 100f / 255f, false)]
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 200f / 255f, false)]
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 100f / 255f, false)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 200f / 255f, true)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, true, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 100f / 255f, true)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 200f / 255f, false)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 100f / 255f, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive,
true, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, false)]
public void ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState(
SurfaceType type,
bool paletted,
string expectedPipeline,
object expectedBlendValue,
float expectedReference,
bool expectedFog)
{
var expectedBlend = (GpuBlendMode)expectedBlendValue;
using var fx = new DispatcherFixture(
withAlphaQueue: true,
detailAvailable: true,
@ -1235,7 +1268,8 @@ public sealed class WalkStaticStreamPopulatorTests
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
detailCategory: 1u,
translucency: kind);
surfaceType: type,
paletted: paletted);
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
@ -1243,11 +1277,121 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
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(
uint detailCategory,
TranslucencyKind translucency = TranslucencyKind.AlphaBlend) =>
SurfaceType surfaceType = SurfaceType.Alpha,
bool paletted = false) =>
new(
new GroupKey(
0,
@ -1255,7 +1399,11 @@ public sealed class WalkStaticStreamPopulatorTests
3,
new GpuTextureSlot(1),
0,
translucency,
TranslucencyKindExtensions.FromSurfaceType(surfaceType),
RetailSetSurfaceMaterialState.Resolve(
surfaceType,
texturePresent: true,
textureHasPalette: paletted),
FoliageFlags: 0),
Matrix4x4.Identity,
ClipSlot: 0,
@ -1277,6 +1425,10 @@ public sealed class WalkStaticStreamPopulatorTests
TextureSlot = new GpuTextureSlot(1),
TextureLayer = 0,
Translucency = TranslucencyKind.AlphaBlend,
MaterialState = RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Alpha,
texturePresent: true,
textureHasPalette: false),
};
group.Matrices.Add(Matrix4x4.Identity);
group.SubmissionOrders.Add(0);
@ -1320,7 +1472,8 @@ public sealed class WalkStaticStreamPopulatorTests
using var fx = new DispatcherFixture(withAlphaQueue: true);
fx.AlphaQueue!.BeginFrame();
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0);
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
@ -1354,7 +1507,8 @@ public sealed class WalkStaticStreamPopulatorTests
var stream = new OrderedDrawStream();
stream.Append(new OrderedDrawCommand(
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using AcDream.Content;
using AcDream.Core.Meshing;
using Chorizite.Core.Render.Enums;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
@ -255,6 +256,60 @@ public sealed class MeshExtractorSolidFaceExtractionTests
BitConverter.SingleToInt32Bits(batch.SurfaceOpacity));
}
public static TheoryData<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>
/// OH2/S1 chunk-2 (2026-09-02): these two winding tests used to register
/// an UNTEXTURED (Base1Solid) surface. Under the exact contract, an
@ -800,7 +855,8 @@ public sealed class MeshExtractorSolidFaceExtractionTests
SurfaceType surfaceType,
PixelFormat pixelFormat,
byte[] sourceData,
float translucency = 0.0f)
float translucency = 0.0f,
uint paletteId = 0)
{
dats.RegisterRootGfxObj(GfxObjId, BuildQuadGfxObj(TexturedSurfaceId, noPos: false));
dats.Register(TexturedSurfaceId, new Surface
@ -818,6 +874,7 @@ public sealed class MeshExtractorSolidFaceExtractionTests
Width = 4,
Height = 4,
Format = pixelFormat,
DefaultPaletteId = paletteId,
SourceData = sourceData,
});
}

View file

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

View file

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

View file

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

View file

@ -136,7 +136,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
}
/// <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
/// alone, BEFORE any TextureBatchData payload field is ever
/// deserialized -- so an older pak can never be silently misread as
@ -160,7 +160,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
fs.Position = 36;
Span<byte> buf = stackalloc byte[4];
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
buf, 8u);
buf, 9u);
fs.Write(buf);
}
@ -168,7 +168,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
() => new PakPreparedAssetSource(path, Identity));
Assert.Contains("does not match the installed DAT set", error.Message);
Assert.Contains("Re-bake", error.Message);
Assert.Contains("bake tool 8 != 9", error.Message);
Assert.Contains("bake tool 9 != 10", error.Message);
}
[Fact]

View file

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

View file

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

View file

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

View file

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