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

@ -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()
{