fix(rendering): repair EnvCell retail CLIP state
Exclude deferred EnvCell subsets from the opaque turn, preserve exact CLIP/ALPHA fixed-state groups through leaf replay, and use retail's row-3 override, blend, depth, and texture-class alpha references. Keep the existing building-detail sentinel distinct from the two CLIP references.
Bound rejected source payload, replace no-op allocation proofs with actual EnvCell and particle RHI paths, rebuild checked-in SPIR-V, and correct AP-238/AP-240 plus the S4-c2 evidence record.
Gates: Release 0W/0E; hermetic 16735/0/0; InstalledDat 255 pass/10 known fail/1 skip; shaders 32/32; focused 239/239; allocation 2/2 at 0 B. The evidence/comment repair re-ran Release 0W/0E, shaders 32/32, affected 37/37, and allocation 2/2; mesh_detail.vert.spv remained byte-identical at SHA-256 5346247ab7d606046943e19b28888c814e08dc6cb27cd9750096ac055457eb57.
Mutation proof, with each mutation restored after its named first failure:
1. Restoring the opaque predicate to !IsAdditive fails WholeLeaf_MixedCellDrawsOpaqueAtTurnThenClipAndAlphaAtDrain first at draw count: expected 1, actual 3.
2. Selecting _alphaPipeline for CLIP fails WholeLeaf_ClipDrainBindsExactStateAndTextureClassReference first at the bind sequence: expected [envcell-clip], actual [envcell-alpha].
3. Disabling CLIP depth write fails that production Theory first at Assert.True(clipPipeline.Depth.Write): expected true, actual false.
4. Swapping palette/DDS references fails the DDS row first: expected 0.784313738, actual 0.392156869; the palette row reports the inverse.
5. Mutating mesh_modern.frag from < to <= fails ClipShaders_UseGreaterEqualForThePerRangeReference first at Assert.Contains("if (color.a < alphaCutoff) discard;"): the required source spelling is absent.
6. Restoring row-3 OverrideClipmap=true fails the real-interface clip Theory first at Assert.False: expected false, actual true.
7. Deleting failed-append rollback fails the flush/end/abort rejection-storm rows first at the bounded pending-count assertion: expected 0, actual 9000.
8. Selecting _transparentDetailPipeline for a ClipMap detail contribution fails the leaf detail pin first at the bind sequence: expected second bind envcell-retail-detail-clip, actual envcell-retail-detail-alpha.
9. Resetting detail ParamB to zero fails the same detail pin first at the second pushed reference: expected 0.784313738, actual 0.
10. Classifying CLIP with exact mask equality excludes legal 0x09 and fails WholeLeaf_PositiveStippleClipMaskUsesClipPipelineAndDdsReference first at pipeline: expected envcell-clip, actual envcell-alpha.
11. Mapping the new blend to SRC_ALPHA/INVSRCALPHA fails AllRetailBlendModesAreRepresentable first at the tuple: expected (One, OneMinusSrcAlpha), actual (SrcAlpha, OneMinusSrcAlpha).
12. Restoring mesh_detail.vert's uParamB > 0.5 category predicate fails ClipShaders_UseGreaterEqualForThePerRangeReference first because vDetailCategory = uParamB == 1.0 is absent.
13. Treating every positive detail uParamB as a cutoff fails that source pin first because isRetailClipReference(uParamB) ? uParamB : 0.05 is absent.
14. Adding arbitrary 0.5 as an accepted reference to either mesh_modern.frag or mesh_detail.frag fails that source pin first at Assert.DoesNotContain("value - 0.5"); both mutations were run and reversed independently.
Retail: D3DPolyRender::SetSurface @ 0x0059c4d0; paired binary @ 0x0059c72a, 0x0059c747/0x0059c74f, 0x0059c821, 0x0059c838, 0x0059c866.
This commit is contained in:
parent
0aa166aa09
commit
252886e84f
21 changed files with 812 additions and 277 deletions
|
|
@ -121,6 +121,14 @@ internal enum GpuBlendMode
|
|||
/// <summary>Straight alpha: <c>SrcAlpha, OneMinusSrcAlpha</c>.</summary>
|
||||
StraightAlpha,
|
||||
|
||||
/// <summary>
|
||||
/// Premultiplied/source-over: <c>One, OneMinusSrcAlpha</c>. Retail pure
|
||||
/// Base1ClipMap surfaces select exactly this pair in
|
||||
/// <c>D3DPolyRender::SetSurface</c> @0x0059c72a when the per-entry
|
||||
/// override-clipmap byte is clear.
|
||||
/// </summary>
|
||||
PremultipliedAlpha,
|
||||
|
||||
/// <summary>Additive: <c>SrcAlpha, One</c>.</summary>
|
||||
Additive,
|
||||
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ internal static class VulkanViewportMapping
|
|||
internal static (BlendFactor Source, BlendFactor Destination) BlendFactorsOf(GpuBlendMode blend) => blend switch
|
||||
{
|
||||
GpuBlendMode.StraightAlpha => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
||||
GpuBlendMode.PremultipliedAlpha => (BlendFactor.One, BlendFactor.OneMinusSrcAlpha),
|
||||
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
||||
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
||||
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ public sealed unsafe partial class ParticleRenderer
|
|||
|
||||
/// <summary>
|
||||
/// The RHI arm's constructor. No GL context, no <c>Shader</c>, no
|
||||
/// <c>BindlessSupport</c>: the five pipelines compile <c>particle</c> and
|
||||
/// <c>BindlessSupport</c>: the six pipelines compile <c>particle</c> and
|
||||
/// <c>particle_mesh</c> from the committed SPIR-V, and both texture sources
|
||||
/// already hand out the device's own <c>GpuTextureSlot</c> (V4t).
|
||||
/// </summary>
|
||||
|
|
@ -267,9 +267,11 @@ public sealed unsafe partial class ParticleRenderer
|
|||
});
|
||||
|
||||
/// <summary>
|
||||
/// One mesh-particle pipeline. Same depth bracket as the billboards (see
|
||||
/// <see cref="AcDream.App.Rendering.WorldDepthContract"/> for the world
|
||||
/// <c>GL_LESS</c> citation); the winding is CW because
|
||||
/// One mesh-particle pipeline. Every variant uses retail's world
|
||||
/// <c>GL_LESS</c> compare; delayed alpha variants match the billboard
|
||||
/// no-write bracket, while row 5's opaque variant depth-writes. See
|
||||
/// <see cref="AcDream.App.Rendering.WorldDepthContract"/> for the compare
|
||||
/// citation. The winding is CW because
|
||||
/// <c>PrepareMeshPipeline</c> sets <c>glFrontFace(GL_CW)</c>, and the cull
|
||||
/// mode stays DYNAMIC because it is resolved per sub-batch from the DAT's
|
||||
/// own <c>CullMode</c>.
|
||||
|
|
|
|||
|
|
@ -263,13 +263,31 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||
{
|
||||
int token = clipSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
||||
// This fixed source is ordinary DrawMesh row 3: pure ClipMap,
|
||||
// multipass false, therefore CLIP with overrideClipmap=false.
|
||||
// If the queue drops at 3,000, retain no source-owned payload;
|
||||
// TryAppend has already registered the source for exact reset.
|
||||
if (!queue.TryAppend(
|
||||
RetailAlphaList.Clip,
|
||||
clipSource,
|
||||
token,
|
||||
overrideClipmap: false))
|
||||
{
|
||||
clipSource.RollbackPendingCellId(token);
|
||||
}
|
||||
}
|
||||
|
||||
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||
{
|
||||
int token = alphaSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
||||
if (!queue.TryAppend(
|
||||
RetailAlphaList.Alpha,
|
||||
alphaSource,
|
||||
token,
|
||||
overrideClipmap: false))
|
||||
{
|
||||
alphaSource.RollbackPendingCellId(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -297,13 +315,27 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
if ((routes & EnvCellTransparentRoute.Clip) != 0)
|
||||
{
|
||||
int token = clipSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Clip, clipSource, token, overrideClipmap: true);
|
||||
if (!queue.TryAppend(
|
||||
RetailAlphaList.Clip,
|
||||
clipSource,
|
||||
token,
|
||||
overrideClipmap: false))
|
||||
{
|
||||
clipSource.RollbackPendingCellId(token);
|
||||
}
|
||||
}
|
||||
|
||||
if ((routes & EnvCellTransparentRoute.Alpha) != 0)
|
||||
{
|
||||
int token = alphaSource.AddPendingCellId(cellId);
|
||||
queue.TryAppend(RetailAlphaList.Alpha, alphaSource, token, overrideClipmap: false);
|
||||
if (!queue.TryAppend(
|
||||
RetailAlphaList.Alpha,
|
||||
alphaSource,
|
||||
token,
|
||||
overrideClipmap: false))
|
||||
{
|
||||
alphaSource.RollbackPendingCellId(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -327,15 +359,26 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
EnvCellTransparentRoute route,
|
||||
bool detailSurfaceActive);
|
||||
|
||||
internal sealed class EnvCellAlphaDrawSource(
|
||||
RenderEnvCellsByRoute renderTransparentOrdered,
|
||||
EnvCellTransparentRoute route)
|
||||
: IRetailAlphaDrawSource
|
||||
internal sealed class EnvCellAlphaDrawSource : IRetailAlphaDrawSource
|
||||
{
|
||||
private readonly RenderEnvCellsByRoute _renderTransparentOrdered;
|
||||
private readonly EnvCellTransparentRoute _route;
|
||||
private readonly Action? _resetObserver;
|
||||
private readonly List<uint> _pendingCellIds = new();
|
||||
private readonly List<uint> _preparedCellIds = new();
|
||||
private readonly List<uint> _drawScratch = new();
|
||||
|
||||
internal EnvCellAlphaDrawSource(
|
||||
RenderEnvCellsByRoute renderTransparentOrdered,
|
||||
EnvCellTransparentRoute route,
|
||||
Action? resetObserver = null)
|
||||
{
|
||||
_renderTransparentOrdered = renderTransparentOrdered
|
||||
?? throw new ArgumentNullException(nameof(renderTransparentOrdered));
|
||||
_route = route;
|
||||
_resetObserver = resetObserver;
|
||||
}
|
||||
|
||||
/// <summary>Returns this cell's token — its index into
|
||||
/// <see cref="_pendingCellIds"/> at the moment it was added.</summary>
|
||||
internal int AddPendingCellId(uint cellId)
|
||||
|
|
@ -345,6 +388,22 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
return token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rolls back only the just-reserved tail token after queue rejection.
|
||||
/// Accepted indices therefore never move, while a rejection storm can
|
||||
/// neither grow the pending count nor force a second geometric growth
|
||||
/// beyond the 3,000-entry list's warmed capacity.
|
||||
/// </summary>
|
||||
internal void RollbackPendingCellId(int token)
|
||||
{
|
||||
if (token != _pendingCellIds.Count - 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Only the just-reserved EnvCell alpha token can be rolled back.");
|
||||
}
|
||||
_pendingCellIds.RemoveAt(token);
|
||||
}
|
||||
|
||||
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
|
||||
{
|
||||
_preparedCellIds.Clear();
|
||||
|
|
@ -359,7 +418,10 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
_drawScratch.Clear();
|
||||
for (int i = 0; i < drawCount; i++)
|
||||
_drawScratch.Add(_preparedCellIds[firstPreparedDraw + i]);
|
||||
renderTransparentOrdered(_drawScratch, route, detailSurfaceActive: false);
|
||||
_renderTransparentOrdered(
|
||||
_drawScratch,
|
||||
_route,
|
||||
detailSurfaceActive: false);
|
||||
}
|
||||
|
||||
public void ResetAlphaSubmissions()
|
||||
|
|
@ -367,9 +429,13 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin
|
|||
_pendingCellIds.Clear();
|
||||
_preparedCellIds.Clear();
|
||||
_drawScratch.Clear();
|
||||
_resetObserver?.Invoke();
|
||||
}
|
||||
|
||||
internal int PendingCount => _pendingCellIds.Count;
|
||||
internal int PendingCapacity => _pendingCellIds.Capacity;
|
||||
internal int PreparedCapacity => _preparedCellIds.Capacity;
|
||||
internal int DrawCapacity => _drawScratch.Capacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ in flat uint vBatchFlags;
|
|||
in flat uint vDetailCategory;
|
||||
|
||||
uniform uint uTextureIndexA; // category detail texture, layer 0
|
||||
// S4-c2 F3: 100/255 or 200/255 for a pure ClipMap base/detail pair. Exact 1
|
||||
// remains mesh_detail.vert's existing building-category sentinel and must use
|
||||
// the ordinary empty-fragment cutoff. Zero is the ordinary EnvCell category.
|
||||
uniform float uParamB;
|
||||
|
||||
// SceneLighting UBO — IDENTICAL layout to mesh_modern.frag binding=1 (same
|
||||
// std140 block, same struct, same binding). Declared here ONLY for fog
|
||||
|
|
@ -50,6 +54,11 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
|||
|
||||
out vec4 FragColor;
|
||||
|
||||
bool isRetailClipReference(float value) {
|
||||
return abs(value - (100.0 / 255.0)) < 0.000001
|
||||
|| abs(value - (200.0 / 255.0)) < 0.000001;
|
||||
}
|
||||
|
||||
// VM2 (2026-08-22, live cdb read on the PDB-paired retail client, GUID
|
||||
// 9e847e2f-777c-4bd9-886c-22256bb87f32): retail's RenderDevice reports
|
||||
// m_caps.bCanDoSinglePassDetailing = 1 and the file-static trysinglepass = 1,
|
||||
|
|
@ -100,7 +109,8 @@ void main() {
|
|||
vec4 base = ACDREAM_SAMPLE_ARRAY(
|
||||
vBaseTextureIndex,
|
||||
vec3(vBaseUv, float(vBaseTextureLayer)));
|
||||
if (base.a < 0.05)
|
||||
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
||||
if (base.a < alphaCutoff)
|
||||
discard;
|
||||
|
||||
vec4 detail = ACDREAM_SAMPLE_ARRAY(
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
|||
uint instanceClipSlot[];
|
||||
};
|
||||
|
||||
// Object renderer only: 1 for a retail building shell, 0 for ordinary
|
||||
// scenery/creatures/players. EnvCellRenderer sets uParamB=0 and ignores the
|
||||
// value, but still binds one valid word because Vulkan sees this static use.
|
||||
// Exact uParamB=1 selects this per-instance building/object category. EnvCell
|
||||
// zero and its pure-ClipMap 100/255 or 200/255 references retain the bound
|
||||
// environment category; the fragment shader consumes those CLIP references.
|
||||
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
||||
uint instanceDetailCategory[];
|
||||
};
|
||||
|
|
@ -61,7 +61,10 @@ uniform mat4 uViewProjection;
|
|||
uniform int uDrawIDOffset;
|
||||
uniform uint uTextureIndexB; // absolute transform prefix in the shared pose arena
|
||||
uniform float uParamA; // detail UV tiling
|
||||
uniform float uParamB; // 1 = require building instance, 0 = EnvCell category
|
||||
// Exact 1 remains the existing "require building instance" sentinel. S4-c2
|
||||
// also passes 100/255 or 200/255 for an EnvCell ClipMap alpha reference; both
|
||||
// must retain the EnvCell category rather than taking the building branch.
|
||||
uniform float uParamB;
|
||||
|
||||
out vec2 vBaseUv;
|
||||
out vec2 vDetailUv;
|
||||
|
|
@ -95,7 +98,7 @@ void main() {
|
|||
vBaseTextureIndex = batch.textureIndex;
|
||||
vBaseTextureLayer = batch.textureLayer;
|
||||
vBatchFlags = batch.flags;
|
||||
vDetailCategory = uParamB > 0.5
|
||||
vDetailCategory = uParamB == 1.0
|
||||
? instanceDetailCategory[instanceIndex]
|
||||
: 1u;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
|||
// transparent overdraw cost otherwise)
|
||||
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
|
||||
// recognized, supplied per MDI range from TextureKey.PaletteId (100/255
|
||||
// paletted, 200/255 DDS/non-paletted). Every other value keeps 0.05.
|
||||
uniform float uParamB;
|
||||
|
||||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||||
struct Light {
|
||||
|
|
@ -57,15 +61,23 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
|||
|
||||
out vec4 FragColor;
|
||||
|
||||
bool isRetailClipReference(float value) {
|
||||
return abs(value - (100.0 / 255.0)) < 0.000001
|
||||
|| abs(value - (200.0 / 255.0)) < 0.000001;
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
||||
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
||||
|
||||
// Two-pass alpha-test (N.5 Decision 2).
|
||||
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
|
||||
// derives the MSAA sample mask from it — ClipMap foliage edges become smooth.
|
||||
// Discard only fully-transparent (α < 0.05); the GPU handles coverage masking.
|
||||
if (uRenderPass == 0) {
|
||||
if (color.a < 0.05) discard; // opaque pass — kill truly empty only (A2C)
|
||||
// '<' spells D3DCMP_GREATEREQUAL: equality passes. Retail loads
|
||||
// 100 or 200 and func 7 at SetSurface 0x0059c73c..0x0059c838.
|
||||
if (color.a < alphaCutoff) discard;
|
||||
} else {
|
||||
// Transparent pass.
|
||||
//
|
||||
|
|
@ -84,7 +96,7 @@ void main() {
|
|||
// Keep the α<0.05 short-circuit as a fragment-cost optimization
|
||||
// (skip fully-empty pixels — saves blend bandwidth on alpha-keyed
|
||||
// sprites with large transparent margins).
|
||||
if (color.a < 0.05) discard;
|
||||
if (color.a < alphaCutoff) discard;
|
||||
}
|
||||
|
||||
// Per-vertex Gouraud lighting from the vertex shader (ambient + capped
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -231,12 +231,12 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "0c1eddb0fc892cb76ef4da47f727cec2516052d63247fa2682918bfd60b84fa8",
|
||||
"sourceSha256": "2d8b5b42f053a2d42e1ff44db3e331d5bea2d3f7dd36e20d813b7ae22c033181",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "e037fd28cf71453792c17522c97c4c7797193823ed78ca629a4331b632f177b1",
|
||||
"sourceSha256": "a5285c51e8d6a1fa91f403f8489819595fa5a20abaaaccc969f220ea2c4eb335",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
@ -252,7 +252,7 @@
|
|||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "12f2ceec9a8420bd273650e95251ca56c3bf3c3691fce1a539e4c5aea7abaabf",
|
||||
"sourceSha256": "d6ae09de279e814b159e7c481d6b66f3cc37c741b3d8a249fbe2bc8156e52d4e",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
private readonly IWorldPassScope? _scope;
|
||||
private IGpuPipeline? _opaquePipeline;
|
||||
private IGpuPipeline? _alphaPipeline;
|
||||
private IGpuPipeline? _clipPipeline;
|
||||
private IGpuPipeline? _additivePipeline;
|
||||
private IGpuPipeline? _detailPipeline;
|
||||
private IGpuPipeline? _transparentDetailPipeline;
|
||||
private IGpuPipeline? _clipDetailPipeline;
|
||||
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
||||
private readonly Func<bool> _buildingDetailEnabled;
|
||||
|
||||
|
|
@ -48,7 +50,7 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
|
||||
/// <summary>
|
||||
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
||||
/// five pipelines ARE this renderer's program, so there is no second step
|
||||
/// seven pipelines ARE this renderer's program, so there is no second step
|
||||
/// and no <c>Shader</c> to hand in.
|
||||
/// </summary>
|
||||
internal EnvCellRenderer(
|
||||
|
|
@ -72,6 +74,12 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
device, "envcell-opaque", GpuBlendMode.None, depthWrite: true, scope.SampleCount);
|
||||
_alphaPipeline = CreateShellPipeline(
|
||||
device, "envcell-alpha", GpuBlendMode.StraightAlpha, depthWrite: false, scope.SampleCount);
|
||||
_clipPipeline = CreateShellPipeline(
|
||||
device,
|
||||
"envcell-clip",
|
||||
GpuBlendMode.PremultipliedAlpha,
|
||||
depthWrite: true,
|
||||
scope.SampleCount);
|
||||
_additivePipeline = CreateShellPipeline(
|
||||
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
||||
_detailPipeline = CreateShellPipeline(
|
||||
|
|
@ -92,6 +100,15 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: true));
|
||||
_clipDetailPipeline = CreateShellPipeline(
|
||||
device,
|
||||
"envcell-retail-detail-clip",
|
||||
GpuBlendMode.RetailDetail,
|
||||
depthWrite: true,
|
||||
scope.SampleCount,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: false));
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
|
|
@ -274,12 +291,18 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
int groupIndex = drawRange.GroupIndex;
|
||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||
(CullMode)(groupIndex % 4));
|
||||
(CullMode)(groupIndex % CullGroupCount));
|
||||
|
||||
bool isAdditive = groupIndex >= 4;
|
||||
IGpuPipeline rangeBasePipeline = isAdditive
|
||||
? _additivePipeline!
|
||||
: _alphaPipeline!;
|
||||
bool isAdditive = renderPass == WbRenderPass.Transparent
|
||||
&& groupIndex >= AdditiveGroupBase
|
||||
&& groupIndex < ClipDdsGroupBase;
|
||||
bool isClip = renderPass == WbRenderPass.Transparent
|
||||
&& groupIndex >= ClipDdsGroupBase;
|
||||
IGpuPipeline rangeBasePipeline = isClip
|
||||
? _clipPipeline!
|
||||
: isAdditive
|
||||
? _additivePipeline!
|
||||
: _alphaPipeline!;
|
||||
if (renderPass == WbRenderPass.Transparent)
|
||||
{
|
||||
// Blend state is the pipeline's; switching variants mid-pass has
|
||||
|
|
@ -298,6 +321,9 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
? (int)renderPass | 0x100
|
||||
: (int)renderPass;
|
||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
|
||||
// Retail DrawMesh's two-pass fallback redraws each transparent
|
||||
|
|
@ -317,7 +343,9 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
pushConstants.DrawIdOffset = command;
|
||||
pushConstants.TextureIndexA = 0;
|
||||
pushConstants.ParamA = 0f;
|
||||
pushConstants.ParamB = 0f;
|
||||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
|
|
@ -325,12 +353,17 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
1,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
|
||||
BindPipelineWithMesh(encoder, _transparentDetailPipeline!, mesh);
|
||||
BindPipelineWithMesh(
|
||||
encoder,
|
||||
isClip ? _clipDetailPipeline! : _transparentDetailPipeline!,
|
||||
mesh);
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = command;
|
||||
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _environmentDetail.Tiling;
|
||||
pushConstants.ParamB = 0f;
|
||||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
|
|
@ -348,9 +381,11 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
|
||||
// Retail DrawEnvCell category (2). Replay the already-filtered opaque
|
||||
// shell commands, including ClipMap built-mesh subsets. No distance
|
||||
// fade (VM1/VM2): retail's noFadeDetail gates get_alpha_for_z to the
|
||||
// Retail DrawEnvCell category (2). Replay only the already-filtered
|
||||
// opaque shell commands. Transparent ClipMap is excluded here: it
|
||||
// takes the immediate/delayed CLIP path and, when detail is active,
|
||||
// its own _clipDetailPipeline contribution. No distance fade
|
||||
// (VM1/VM2): retail's noFadeDetail gates get_alpha_for_z to the
|
||||
// immediate-polygon path only, which built meshes never reach;
|
||||
// attenuation is the sampler's linear mip chain converging to the
|
||||
// texture mean. The existing "Building Detail Textures" option gates
|
||||
|
|
@ -368,7 +403,7 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
{
|
||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||
(CullMode)(drawRange.GroupIndex % 4));
|
||||
(CullMode)(drawRange.GroupIndex % CullGroupCount));
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
|
|
@ -468,9 +503,10 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
|
||||
/// <summary>
|
||||
/// Binds one category word for <c>mesh_detail.vert</c>'s statically used
|
||||
/// binding 9. EnvCell draws select their renderer-wide category via
|
||||
/// <c>uParamB=0</c>, so the value is semantically unused, but Vulkan still
|
||||
/// requires the declared descriptor to be valid.
|
||||
/// binding 9. The vertex shader treats only exact <c>uParamB=1</c> as the
|
||||
/// object/building-category sentinel; ordinary EnvCell zero and the CLIP
|
||||
/// references 100/255 and 200/255 therefore keep the environment category.
|
||||
/// Vulkan still requires the declared descriptor to be valid.
|
||||
/// </summary>
|
||||
internal static void BindEnvironmentDetailCategory(
|
||||
IGpuPassEncoder encoder,
|
||||
|
|
@ -491,11 +527,15 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
_opaquePipeline = null;
|
||||
_alphaPipeline?.Dispose();
|
||||
_alphaPipeline = null;
|
||||
_clipPipeline?.Dispose();
|
||||
_clipPipeline = null;
|
||||
_additivePipeline?.Dispose();
|
||||
_additivePipeline = null;
|
||||
_detailPipeline?.Dispose();
|
||||
_detailPipeline = null;
|
||||
_transparentDetailPipeline?.Dispose();
|
||||
_transparentDetailPipeline = null;
|
||||
_clipDetailPipeline?.Dispose();
|
||||
_clipDetailPipeline = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,8 +142,17 @@ public sealed partial class EnvCellRenderer :
|
|||
private readonly List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> _renderDrawCalls = new();
|
||||
private readonly Dictionary<ulong, List<InstanceData>> _filteredGroups = new();
|
||||
private readonly HashSet<List<InstanceData>> _filteredOwnedLists = new();
|
||||
// S4-c2 F3: four cull modes for each fixed-state family. The original
|
||||
// eight slots were non-additive/additive only; transparent replay also
|
||||
// needs distinct pure-ClipMap DDS/paletted ranges because their alpha-test
|
||||
// references differ and neither state may share an MDI call with ALPHA.
|
||||
private const int CullGroupCount = 4;
|
||||
private const int AdditiveGroupBase = 4;
|
||||
private const int ClipDdsGroupBase = 8;
|
||||
private const int ClipPalettedGroupBase = 12;
|
||||
private const int BatchGroupCount = 16;
|
||||
private readonly List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)>[] _batchesByCullGroup =
|
||||
Enumerable.Range(0, 8)
|
||||
Enumerable.Range(0, BatchGroupCount)
|
||||
.Select(_ => new List<(ObjectRenderBatch, int, int)>())
|
||||
.ToArray();
|
||||
private readonly List<int> _activeCullGroups = new(8);
|
||||
|
|
@ -1214,17 +1223,8 @@ public sealed partial class EnvCellRenderer :
|
|||
foreach (var batch in call.renderData.Batches)
|
||||
{
|
||||
// WB BaseObjectRenderManager.cs:723-731: pass-filter.
|
||||
if (renderPass != WbRenderPass.SinglePass)
|
||||
{
|
||||
if (batch.IsAdditive)
|
||||
{
|
||||
if (renderPass == WbRenderPass.Opaque) continue;
|
||||
}
|
||||
else if (!batch.IsTransparent)
|
||||
{
|
||||
if (renderPass == WbRenderPass.Transparent) continue;
|
||||
}
|
||||
}
|
||||
if (!BatchBelongsToPass(batch, renderPass))
|
||||
continue;
|
||||
|
||||
if (renderPass == WbRenderPass.Transparent
|
||||
&& transparentRoute != EnvCellTransparentRoute.All
|
||||
|
|
@ -1277,17 +1277,8 @@ public sealed partial class EnvCellRenderer :
|
|||
var call = drawCalls[callIndex];
|
||||
foreach (var batch in call.renderData.Batches)
|
||||
{
|
||||
if (renderPass != WbRenderPass.SinglePass)
|
||||
{
|
||||
if (batch.IsAdditive)
|
||||
{
|
||||
if (renderPass == WbRenderPass.Opaque) continue;
|
||||
}
|
||||
else if (!batch.IsTransparent)
|
||||
{
|
||||
if (renderPass == WbRenderPass.Transparent) continue;
|
||||
}
|
||||
}
|
||||
if (!BatchBelongsToPass(batch, renderPass))
|
||||
continue;
|
||||
|
||||
if (renderPass == WbRenderPass.Transparent
|
||||
&& transparentRoute != EnvCellTransparentRoute.All
|
||||
|
|
@ -1296,7 +1287,9 @@ public sealed partial class EnvCellRenderer :
|
|||
continue;
|
||||
}
|
||||
|
||||
int groupIndex = (int)batch.CullMode + (batch.IsAdditive ? 4 : 0);
|
||||
int groupIndex = ResolveBatchGroupIndex(
|
||||
batch,
|
||||
renderPass);
|
||||
List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)> group =
|
||||
_batchesByCullGroup[groupIndex];
|
||||
if (group.Count == 0)
|
||||
|
|
@ -1357,6 +1350,50 @@ public sealed partial class EnvCellRenderer :
|
|||
SubmitRhi(allInstances, renderPass, totalDraws, uniqueInstanceCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exact WB pass membership, with <see cref="WbRenderPass.SinglePass"/>
|
||||
/// intentionally unchanged. A non-additive transparent batch is delayed
|
||||
/// content and must never draw during the preceding opaque cell turn.
|
||||
/// </summary>
|
||||
internal static bool BatchBelongsToPass(
|
||||
ObjectRenderBatch batch,
|
||||
WbRenderPass renderPass) => renderPass switch
|
||||
{
|
||||
WbRenderPass.Opaque => !batch.IsAdditive && !batch.IsTransparent,
|
||||
WbRenderPass.Transparent => batch.IsAdditive || batch.IsTransparent,
|
||||
WbRenderPass.SinglePass => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static int ResolveBatchGroupIndex(
|
||||
ObjectRenderBatch batch,
|
||||
WbRenderPass renderPass)
|
||||
{
|
||||
int cull = (int)batch.CullMode;
|
||||
if ((uint)cull >= CullGroupCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(batch), batch.CullMode, "Unknown cell-shell cull mode.");
|
||||
|
||||
// Preserve the old SinglePass grouping exactly: non-additive 0..3,
|
||||
// additive 4..7. Its submission path still keeps the opaque pipeline.
|
||||
if (renderPass != WbRenderPass.Transparent)
|
||||
return cull + (batch.IsAdditive ? AdditiveGroupBase : 0);
|
||||
|
||||
if (batch.IsAdditive)
|
||||
return cull + AdditiveGroupBase;
|
||||
|
||||
// Fixed state follows ConstructMesh's retained per-subset class, not
|
||||
// the DrawMesh row action. Detail-active row 1 is Immediate, but a
|
||||
// mask-0x08 subset (including legal positive-stipple 0x09) still needs
|
||||
// the CLIP base pipeline/reference. ConstructMesh's priority already
|
||||
// excludes additive/alpha-family rows before retaining this class.
|
||||
if ((batch.RetailSurfaceMask & RetailAlphaMeshRouter.MaskClipMap) == 0)
|
||||
return cull;
|
||||
|
||||
return cull + (batch.Key.PaletteId != 0
|
||||
? ClipPalettedGroupBase
|
||||
: ClipDdsGroupBase);
|
||||
}
|
||||
|
||||
internal static void AppendMdiDrawRange(
|
||||
List<MdiDrawRange> ranges,
|
||||
int groupIndex,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue