feat(rendering): port retail one-pass detail material
Replace the building and EnvCell detail replay with retail's exact single-pass stage result, including authored surface opacity, squared detail alpha, final-alpha clipping, and the original subset pipeline/order. Arm the ordered walk command in place to close #471, delete the replay pipelines/shaders, and advance prepared content to recipe 9. Mutation witnesses (each restored before commit): - X=a*qA: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 174, missing materialAlpha * detail.a * detail.a. - X*=base alpha: same test line 175, forbidden baseTexel.a found. - CLIP against base alpha: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 260, final-X conditional missing. - second detail draw: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 105, Assert.Single saw 2 MDI calls. - straight-alpha substitution: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_RetainsOriginalFramebufferFamily line 1244, Additive first failed (only wb-mesh-alpha-1x recorded; InvAlpha also failed). - omit ordered arm: OrderPreservingSubmitterTests.PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace line 305, expected (77,3.5), got (0,0). - omit atmospheric combine: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 173, atmospheric shared include missing. - drop serialized opacity: ObjectMeshDataSerializerTests.SurfaceOpacity_RoundTripsBitExactlyAndDeterministically line 288, first reported 0.5 bits 1056964608 vs 1065353216. - stale detail arm: ordered adjacency test line 307, expected following ordinary (0,0), got (77,3.5). - per-frame surface map: EnvCellAlphaDrawSourceTests.ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate line 178, expected 0 B, got 147456 B. Verification before commit: shader compiler 23/23; focused App 213/213; Content 75/75; Core Wb 10/10; launcher migration 6/6; Release solution build 0 warnings / 0 errors; git diff --check clean.
This commit is contained in:
parent
13dbb79eeb
commit
75664805f8
61 changed files with 1003 additions and 1016 deletions
|
|
@ -143,17 +143,6 @@ internal enum GpuBlendMode
|
|||
/// `ParticleRenderer` needs it too (slice V4e).
|
||||
/// </summary>
|
||||
InverseAlpha,
|
||||
|
||||
/// <summary>
|
||||
/// Retail building/EnvCell detail overlay: <c>SrcAlpha,
|
||||
/// OneMinusSrcAlpha</c>. VM2's live cdb read proved retail hardware runs
|
||||
/// the single-pass texture-stage combine (<c>D3DPolyRender::SetSurface</c>
|
||||
/// 0x0059c4d0), a lerp toward the detail colour by
|
||||
/// <c>detail.a * diffuse.a</c> — a mild darkening on the live Dereth
|
||||
/// category texture, not the two-pass <c>DstColor</c> fallback's
|
||||
/// brightening.
|
||||
/// </summary>
|
||||
RetailDetail,
|
||||
}
|
||||
|
||||
internal enum GpuCompareOp
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ internal struct GpuPushConstants
|
|||
public uint TextureIndexA;
|
||||
|
||||
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot; terrain
|
||||
/// uses it for the alpha-mask array, while shared-pose world/detail passes
|
||||
/// uses it for the alpha-mask array, while shared-pose world passes
|
||||
/// carry the absolute transform-prefix instance count.</summary>
|
||||
public uint TextureIndexB;
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ internal sealed record VulkanDeviceFeatureSupport
|
|||
/// <summary>
|
||||
/// #459: the shader compiler targets Vulkan 1.3 / SPIR-V 1.6, where glslang
|
||||
/// lowers every fragment <c>discard</c> to <c>OpDemoteToHelperInvocation</c>
|
||||
/// (five committed modules declare the capability — sky, mesh_detail,
|
||||
/// (five committed modules declare the capability — sky, mesh_modern,
|
||||
/// mesh_atmospheric and both directional-shadow cutout fragments). A module
|
||||
/// that declares the capability without the feature enabled is invalid
|
||||
/// usage (<c>VUID-VkShaderModuleCreateInfo-pCode-08740</c>), so the feature
|
||||
|
|
|
|||
|
|
@ -178,12 +178,6 @@ internal static class VulkanViewportMapping
|
|||
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
||||
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
||||
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||
// VM2 (live cdb read, 2026-08-22): real hardware runs the single-pass
|
||||
// texture-stage detail combine, not the two-pass DESTCOLOR fallback.
|
||||
// D3DPolyRender::SetSurface's stage-1 BLENDCURRENTALPHA is a lerp
|
||||
// toward the detail colour by detail.a * diffuse.a, which is the
|
||||
// ordinary straight-alpha-over factor pair.
|
||||
GpuBlendMode.RetailDetail => (BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
||||
GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(blend), blend, "Unknown blend mode."),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Testable CPU statement of retail's detail-pass gate and pixel math. The
|
||||
/// production pixels are produced by <c>mesh_detail</c>; keeping these facts
|
||||
/// Testable CPU statement of retail's detail gate and one-pass pixel math. The
|
||||
/// production pixels are produced by both world-mesh fragment families; keeping these facts
|
||||
/// in one small contract makes the setting and the combine independently
|
||||
/// assertable without a GPU.
|
||||
///
|
||||
|
|
@ -17,15 +16,12 @@ namespace AcDream.App.Rendering;
|
|||
/// <c>D3DPolyRender::RenderMeshSubset</c> (0x0059ca10) never falls back to
|
||||
/// the two-pass framebuffer blend the earlier #226 port reproduced; it takes
|
||||
/// the single-pass texture-stage combine set up in
|
||||
/// <c>D3DPolyRender::SetSurface</c> (0x0059c4d0):
|
||||
/// <c>lerp(base * diffuse, detail.rgb, detail.a * diffuse.a)</c> — a blend
|
||||
/// TOWARD the detail colour by <c>detail.a * diffuse.a</c>, not the
|
||||
/// fallback's <c>dest * (detail.rgb + 1 - detail.a)</c>. Built meshes light
|
||||
/// with <c>tmpmaterial.Diffuse.a = 1</c> for opaque subsets
|
||||
/// (<c>RenderMeshSubset</c>), so on the live Dereth category texture (mean
|
||||
/// rgb 0.165, mean alpha 0.132) the combine is a mild darkening
|
||||
/// (≈ 0.868 * base + 0.022), the opposite sign of the fallback's
|
||||
/// brightening.</para>
|
||||
/// <c>D3DPolyRender::SetSurface</c> (0x0059c4d0). With base RGB <c>B</c>,
|
||||
/// diffuse RGB <c>D</c>, active material alpha <c>a</c>, and detail texel
|
||||
/// <c>Q=(q,qA)</c>, that stage program emits
|
||||
/// <c>C=q*(a*qA)+(B*D)*(1-a*qA)</c> and <c>X=a*qA*qA</c>. Base texture alpha
|
||||
/// is deliberately excluded. This is not the fallback's framebuffer replay
|
||||
/// result <c>dest * (detail.rgb + 1 - detail.a)</c>.</para>
|
||||
///
|
||||
/// <para>There is no distance fade on this path. Retail's
|
||||
/// <c>ACRender::get_alpha_for_z</c> (0x006b6230) is only evaluated in
|
||||
|
|
@ -44,66 +40,69 @@ internal static class RetailDetailTextureContract
|
|||
settingEnabled && binding.IsAvailable;
|
||||
|
||||
/// <summary>
|
||||
/// Opaque detail must compare equal against the depth written by its exact
|
||||
/// base geometry. On an MSAA target that inherits the base pass's per-sample
|
||||
/// alpha-to-coverage mask without applying A2C to the detail alpha itself.
|
||||
/// Transparent bases do not write depth, so their adjacent detail uses the
|
||||
/// accepted less-or-equal comparison instead.
|
||||
/// Retail's exact pre-fog stage result. Base alpha is deliberately absent.
|
||||
/// </summary>
|
||||
internal static GpuCompareOp DetailDepthCompare(bool transparent) =>
|
||||
transparent ? GpuCompareOp.LessOrEqual : GpuCompareOp.Equal;
|
||||
|
||||
/// <summary>
|
||||
/// The exact pixel <c>mesh_detail</c> composites onto the existing
|
||||
/// framebuffer colour: retail's single-pass stage-1
|
||||
/// <c>BLENDCURRENTALPHA(TEXTURE, CURRENT)</c>, a lerp from
|
||||
/// <paramref name="baseColour"/> toward <paramref name="detail"/>'s RGB by
|
||||
/// <c>detail.a * opacity</c>. <paramref name="opacity"/> is the base
|
||||
/// subset's diffuse alpha — 1 for an opaque subset, the translucency-fade
|
||||
/// multiplier for a fading one — mirrored from the shader's
|
||||
/// <c>instanceAlpha[instanceIndex]</c> read.
|
||||
/// </summary>
|
||||
internal static Vector3 Expected(Vector3 baseColour, Vector4 detail, float opacity) =>
|
||||
Vector3.Lerp(
|
||||
baseColour,
|
||||
new Vector3(detail.X, detail.Y, detail.Z),
|
||||
detail.W * opacity);
|
||||
|
||||
/// <summary>
|
||||
/// True when the combine above is an exact no-op — either the detail
|
||||
/// texel is fully transparent or the base subset's own diffuse alpha (the
|
||||
/// translucency fade) has reached zero. Neutral is <c>detail.a * opacity
|
||||
/// == 0</c>, not any particular colour equality.
|
||||
/// </summary>
|
||||
internal static bool IsNeutral(Vector4 detail, float opacity) =>
|
||||
detail.W * opacity == 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Review fix (post-05970306): retail's D3D fog stage runs AFTER the
|
||||
/// texture-stage combine, applying to the FINAL pixel, not to
|
||||
/// <c>detail.rgb</c> in isolation. acdream draws the combine as two
|
||||
/// separate passes (mesh_modern's base draw, then mesh_detail's blended
|
||||
/// replay), so each draw fogs its OWN colour before the fixed-function
|
||||
/// blend recombines them — this is the CPU statement of that two-draw
|
||||
/// path: <c>lerp(mix(base,fog,f), mix(detail,fog,f), detail.a*opacity)</c>.
|
||||
/// It is algebraically identical to retail's single-draw
|
||||
/// fog-after-combine order, <c>mix(Expected(base,detail,opacity), fog,
|
||||
/// f)</c> — see <c>RetailDetailTextureContractTests</c> for the identity
|
||||
/// pinned numerically, and mesh_detail.frag's header comment for the
|
||||
/// derivation.
|
||||
/// </summary>
|
||||
internal static Vector3 ExpectedFogged(
|
||||
Vector3 baseColour,
|
||||
internal static Vector4 Combine(
|
||||
Vector4 baseTexel,
|
||||
Vector3 diffuse,
|
||||
Vector4 detail,
|
||||
float opacity,
|
||||
Vector3 fog,
|
||||
float fogFactor)
|
||||
float authoredOpacity,
|
||||
float liveOpacity)
|
||||
{
|
||||
Vector3 foggedBase = Vector3.Lerp(baseColour, fog, fogFactor);
|
||||
Vector3 foggedDetail = Vector3.Lerp(
|
||||
new Vector3(detail.X, detail.Y, detail.Z),
|
||||
fog,
|
||||
fogFactor);
|
||||
return Vector3.Lerp(foggedBase, foggedDetail, detail.W * opacity);
|
||||
float alpha = authoredOpacity * liveOpacity;
|
||||
float weight = alpha * detail.W;
|
||||
Vector3 colour = new(detail.X, detail.Y, detail.Z);
|
||||
colour *= weight;
|
||||
colour += new Vector3(baseTexel.X, baseTexel.Y, baseTexel.Z)
|
||||
* diffuse * (1f - weight);
|
||||
float outputAlpha = alpha * detail.W * detail.W;
|
||||
return new Vector4(colour, outputAlpha);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the subset's post-material fog to RGB. Retail fog never changes
|
||||
/// the detail-active final alpha.
|
||||
/// </summary>
|
||||
internal static Vector4 ApplyFog(Vector4 material, Vector3 fog, float fogFactor) =>
|
||||
new(Vector3.Lerp(new Vector3(material.X, material.Y, material.Z), fog, fogFactor), material.W);
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors D3D's GREATER_EQUAL alpha test: equality survives.
|
||||
/// </summary>
|
||||
internal static bool SurvivesClip(float outputAlpha, float reference) =>
|
||||
outputAlpha >= reference;
|
||||
|
||||
/// <summary>
|
||||
/// The retained framebuffer-family equations applied to retail's one-pass
|
||||
/// source fragment.
|
||||
/// </summary>
|
||||
internal enum FramebufferFamily
|
||||
{
|
||||
Opaque,
|
||||
Alpha,
|
||||
AlphaAdditive,
|
||||
Additive,
|
||||
InverseAlpha,
|
||||
InverseAlphaAdditive,
|
||||
Clip,
|
||||
}
|
||||
|
||||
internal static Vector4 Composite(
|
||||
Vector4 source,
|
||||
Vector4 destination,
|
||||
FramebufferFamily family)
|
||||
{
|
||||
float x = source.W;
|
||||
return family switch
|
||||
{
|
||||
FramebufferFamily.Opaque => source,
|
||||
FramebufferFamily.Alpha => source * x + destination * (1f - x),
|
||||
FramebufferFamily.AlphaAdditive => source * x + destination,
|
||||
FramebufferFamily.Additive => source + destination,
|
||||
FramebufferFamily.InverseAlpha => source * (1f - x) + destination * x,
|
||||
FramebufferFamily.InverseAlphaAdditive => source * (1f - x) + destination,
|
||||
FramebufferFamily.Clip => source + destination * new Vector4(1f - x),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(family)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,12 @@ in flat uint vTextureLayer;
|
|||
in flat float vOpacityMultiplier; // #188
|
||||
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
||||
in flat uint vReceivesDirectionalShadow;
|
||||
in flat float vSurfaceOpacity;
|
||||
in flat uint vBatchFlags;
|
||||
in flat uint vDetailCategory;
|
||||
|
||||
#include "directional_shadow_receiver.glsl"
|
||||
#include "retail_detail_material.glsl"
|
||||
|
||||
// uRenderPass values (Phase N.5 Decision 2 — two-pass alpha-test):
|
||||
// 0 = opaque pass — discard fragments with alpha < 0.95
|
||||
|
|
@ -26,6 +30,9 @@ in flat uint vReceivesDirectionalShadow;
|
|||
// transparent overdraw cost otherwise)
|
||||
uniform int uRenderPass;
|
||||
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
||||
uniform uint uTextureIndexA;
|
||||
uniform float uParamA;
|
||||
uniform float uParamB;
|
||||
|
||||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||||
struct Light {
|
||||
|
|
@ -61,35 +68,14 @@ 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)));
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
// Transparent pass.
|
||||
//
|
||||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): do NOT discard α≥0.95 here.
|
||||
// Native AC transparent-flagged surfaces routinely include
|
||||
// effectively-opaque pixels — e.g. the Holtburg lifestone crystal core
|
||||
// (surface 0x080011DE) which the spawn manifest classifies as
|
||||
// transparent (batch.IsTransparent=True) but whose decoded texture
|
||||
// alpha lands ≥0.95 across the visible surface. Those pixels still
|
||||
// compose correctly under (SrcAlpha, 1-SrcAlpha) alpha-blending, so
|
||||
// discarding them here threw away the whole crystal. The original
|
||||
// N.5 §2 rationale (high-α fragments belong in the opaque pass) does
|
||||
// not apply when the SURFACE is dat-flagged transparent — those
|
||||
// pixels can't reach the opaque pass at all.
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
||||
|
||||
// Only the authored outdoor directional term is shadowed. Ambient, local
|
||||
// lights and material selection pulses remain exactly on the retail path.
|
||||
|
|
@ -110,6 +96,7 @@ void main() {
|
|||
// ignored). Stripes visible HERE = a vertex-lighting artifact; absent =
|
||||
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
|
||||
if (uLightDebug == 3) {
|
||||
if (color.a < alphaCutoff) discard;
|
||||
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
|
||||
return;
|
||||
}
|
||||
|
|
@ -120,11 +107,30 @@ void main() {
|
|||
// Retail clamp per-channel to 1.0 (r13 §13.1).
|
||||
lit = min(lit, vec3(1.0));
|
||||
|
||||
vec3 rgb = color.rgb * lit;
|
||||
bool detailActive = uParamA != 0.0
|
||||
&& vDetailCategory != 0u
|
||||
&& (vBatchFlags & 1u) != 0u;
|
||||
vec3 rgb;
|
||||
float alpha;
|
||||
if (detailActive) {
|
||||
vec4 detail = ACDREAM_SAMPLE_ARRAY(
|
||||
uTextureIndexA,
|
||||
vec3(vTexCoord * uParamA, 0.0));
|
||||
RetailDetailMaterialResult material = acdreamRetailDetailMaterial(
|
||||
color.rgb,
|
||||
lit,
|
||||
detail,
|
||||
vSurfaceOpacity * vOpacityMultiplier);
|
||||
rgb = material.rgb;
|
||||
alpha = material.alpha;
|
||||
} else {
|
||||
rgb = color.rgb * lit;
|
||||
alpha = color.a * vOpacityMultiplier;
|
||||
}
|
||||
|
||||
if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)
|
||||
discard;
|
||||
|
||||
rgb = applyFog(rgb, vWorldPos);
|
||||
// #188: multiply the FINAL alpha only — the discard thresholds above stay
|
||||
// keyed on the raw sampled color.a, so the last few frames of a fade
|
||||
// (multiplier crossing under 0.05) still ramp smoothly toward zero rather
|
||||
// than popping invisible early against the discard cutoff.
|
||||
FragColor = vec4(rgb, color.a * vOpacityMultiplier);
|
||||
FragColor = vec4(rgb, alpha);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ struct InstanceData {
|
|||
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
||||
struct BatchData {
|
||||
uint textureIndex; // slot into the global texture table
|
||||
uint _pad; // keeps textureLayer/flags at offsets 8/12
|
||||
float surfaceOpacity; // authored material alpha (not base texture alpha)
|
||||
uint textureLayer; // layer in the shared WB or pooled composite array
|
||||
uint flags; // reserved — N.5 dispatcher owns all blend state
|
||||
// (glBlendFunc per pass). If a future phase wants
|
||||
|
|
@ -116,6 +116,10 @@ layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
|
|||
vec2 instanceSelectionLighting[];
|
||||
};
|
||||
|
||||
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
||||
uint instanceDetailCategory[];
|
||||
};
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
// Absolute transform prefix in the shared shadow/world pose arena. Every
|
||||
// parallel per-instance array remains local to this submission, so only the
|
||||
|
|
@ -309,6 +313,9 @@ out flat uint vTextureLayer;
|
|||
out flat float vOpacityMultiplier; // #188
|
||||
out flat vec2 vSelectionLighting;
|
||||
out flat uint vReceivesDirectionalShadow;
|
||||
out flat float vSurfaceOpacity;
|
||||
out flat uint vBatchFlags;
|
||||
out flat uint vDetailCategory;
|
||||
|
||||
void main() {
|
||||
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
|
|
@ -358,4 +365,7 @@ void main() {
|
|||
// could read its flags — Campaign VM VM6.)
|
||||
vTextureIndex = b.textureIndex;
|
||||
vTextureLayer = b.textureLayer;
|
||||
vSurfaceOpacity = b.surfaceOpacity;
|
||||
vBatchFlags = b.flags;
|
||||
vDetailCategory = instanceDetailCategory[instanceIndex];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
#version 430 core
|
||||
#extension GL_ARB_bindless_texture : require
|
||||
|
||||
in vec2 vBaseUv;
|
||||
in vec2 vDetailUv;
|
||||
in float vDetailOpacity;
|
||||
in vec3 vWorldPos;
|
||||
in flat uint vBaseTextureIndex;
|
||||
in flat uint vBaseTextureLayer;
|
||||
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
|
||||
// (uFogParams/uFogColor/uCameraAndTime); mesh_detail never lights (no
|
||||
// uLights[]/uCellAmbient read), it only needs applyFog below to match
|
||||
// retail's fog placement exactly.
|
||||
struct Light {
|
||||
vec4 posAndKind;
|
||||
vec4 dirAndRange;
|
||||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||||
Light uLights[8];
|
||||
vec4 uCellAmbient;
|
||||
vec4 uFogParams;
|
||||
vec4 uFogColor;
|
||||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
// Copied verbatim from mesh_modern.frag — same math, not "improved". Retail's
|
||||
// D3D fixed-function fog stage runs AFTER the texture-stage pipeline
|
||||
// (RenderDeviceD3D's fog render state applies to the final pixel the
|
||||
// blender produced, not to an individual texture stage's output), so the
|
||||
// detail contribution must be fogged exactly like the base pass fogs its
|
||||
// own colour, with the identical fog curve.
|
||||
vec3 applyFog(vec3 lit, vec3 worldPos) {
|
||||
int mode = int(uFogParams.w);
|
||||
if (mode == 0) return lit;
|
||||
float d = length(worldPos - uCameraAndTime.xyz);
|
||||
float fogStart = uFogParams.x;
|
||||
float fogEnd = uFogParams.y;
|
||||
float span = max(1e-3, fogEnd - fogStart);
|
||||
float fog = clamp((d - fogStart) / span, 0.0, 1.0);
|
||||
return mix(lit, uFogColor.xyz, fog);
|
||||
}
|
||||
|
||||
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,
|
||||
// so real hardware never takes the two-pass framebuffer fallback this shader
|
||||
// used to reproduce. It takes the single-pass texture-stage combine set up
|
||||
// in D3DPolyRender::SetSurface (0x0059c4d0) and consumed per built-mesh
|
||||
// material subset by D3DPolyRender::RenderMeshSubset (0x0059ca10):
|
||||
//
|
||||
// stage 0 colour = MODULATE(TEXTURE, DIFFUSE) = base.rgb * diffuse.rgb
|
||||
// stage 0 alpha = PREMODULATE(DIFFUSE, DIFFUSE) = diffuse.a * detail.a
|
||||
// stage 1 colour = BLENDCURRENTALPHA(TEXTURE, CURRENT) = lerp(current.rgb, detail.rgb, stage0.a)
|
||||
//
|
||||
// i.e. the pixel retail draws is lerp(base * diffuse, detail.rgb, detail.a *
|
||||
// diffuse.a) — a blend TOWARD the detail colour by detail.a * diffuse.a, not
|
||||
// a "dest * (detail + 1 - alpha)" brightening. RenderMeshSubset lights every
|
||||
// built mesh with tmpmaterial.Diffuse.a = 1 (opaque subsets), so
|
||||
// vDetailOpacity below is exactly that diffuse.a — 1 for opaque, the
|
||||
// translucency-fade multiplier for a fading subset. The pipeline blend
|
||||
// (VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RetailDetail)) expresses
|
||||
// the same lerp as SrcAlpha + OneMinusSrcAlpha over the existing base pixel,
|
||||
// so this fragment outputs the raw (non-premultiplied) detail colour with
|
||||
// that combined alpha and lets the fixed-function blend unit do the
|
||||
// base*(1-a) + detail*a lerp. See
|
||||
// docs/research/2026-08-22-vm2-retail-detail-path-cdb.md.
|
||||
//
|
||||
// Review fix (post-05970306): retail's texture-stage combine above happens
|
||||
// BEFORE the D3D fixed-function fog stage, not after — fog is the LAST thing
|
||||
// applied to the pixel, so it applies to the lerp's result, not to detail.rgb
|
||||
// alone. mesh_modern.frag already fogs the base colour before this replay
|
||||
// draws over it (applyFog(rgb, vWorldPos) there), so fogging detail.rgb here
|
||||
// too makes the two-draw blend collapse to retail's single-draw order:
|
||||
// (1-a)*mix(base,fog,f) + a*mix(detail,fog,f)
|
||||
// = (1-f)*[(1-a)*base + a*detail] + f*fog
|
||||
// = mix(lerp(base,detail,a), fog, f)
|
||||
// which is exactly retail's fog-after-combine pixel. Leaving detail.rgb
|
||||
// unfogged would draw detail at full saturation/brightness even at maximum
|
||||
// fog distance. RetailDetailTextureContractTests pins this identity
|
||||
// numerically (ExpectedFogged).
|
||||
|
||||
void main() {
|
||||
// Object command replays may contain ordinary instances; only building
|
||||
// shells survive. Bit 0 means this command came through retail's built-mesh
|
||||
// DrawMesh path. Unlike the land-polygon path, RenderMeshSubset receives
|
||||
// curr_detail_surface for every built-mesh material subset.
|
||||
if (vDetailCategory == 0u || (vBatchFlags & 1u) == 0u)
|
||||
discard;
|
||||
|
||||
vec4 base = ACDREAM_SAMPLE_ARRAY(
|
||||
vBaseTextureIndex,
|
||||
vec3(vBaseUv, float(vBaseTextureLayer)));
|
||||
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
||||
if (base.a < alphaCutoff)
|
||||
discard;
|
||||
|
||||
vec4 detail = ACDREAM_SAMPLE_ARRAY(
|
||||
uTextureIndexA,
|
||||
vec3(vDetailUv, 0.0));
|
||||
|
||||
FragColor = vec4(applyFog(detail.rgb, vWorldPos), detail.a * vDetailOpacity);
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
#version 430 core
|
||||
#extension GL_ARB_shader_draw_parameters : require
|
||||
|
||||
layout(location = 0) in vec3 aPosition;
|
||||
layout(location = 1) in vec3 aNormal;
|
||||
layout(location = 2) in vec2 aTexCoord;
|
||||
|
||||
struct InstanceData {
|
||||
mat4 transform;
|
||||
};
|
||||
|
||||
struct BatchData {
|
||||
uint textureIndex;
|
||||
uint _pad;
|
||||
uint textureLayer;
|
||||
uint flags;
|
||||
};
|
||||
|
||||
layout(std430, binding = 0) readonly buffer InstanceBuffer {
|
||||
InstanceData Instances[];
|
||||
};
|
||||
layout(std430, binding = 1) readonly buffer BatchBuffer {
|
||||
BatchData Batches[];
|
||||
};
|
||||
|
||||
// S3 review fix round 1 (F5): the per-cell screen-space gl_ClipDistance gate
|
||||
// (Phase U.3's binding=2 CellClip region SSBO) is deleted — the CPU-side
|
||||
// routing that could ever select a non-zero slot for an instance
|
||||
// (WbDrawDispatcher's per-frame clip-routing arming call) had ZERO production callers, so every
|
||||
// instance has always mapped to slot 0 (no-clip) in every shipped build; a
|
||||
// shader-side clip test against a table that only ever holds the reserved
|
||||
// no-clip slot clips nothing. binding=3 — PER-RENDERER per-instance slot
|
||||
// index, parallel to the binding=0 instance buffer and indexed by the
|
||||
// IDENTICAL per-instance index — stays declared (the CPU side still writes
|
||||
// it, always 0) but is no longer read here; the instance-buffer layout it
|
||||
// occupies is S5's to revisit.
|
||||
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
|
||||
uint instanceClipSlot[];
|
||||
};
|
||||
|
||||
// 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[];
|
||||
};
|
||||
|
||||
// #188 per-instance opacity multiplier, identical binding and indexing to
|
||||
// mesh_modern.vert's InstanceAlphaBuf (binding 7). VM2's cdb read proved
|
||||
// retail's built-mesh detail combine is a single-pass texture-stage blend
|
||||
// whose stage-0 alpha is PREMODULATE(DIFFUSE, DIFFUSE) = diffuse.a *
|
||||
// detail.a (D3DPolyRender::SetSurface 0x0059c4d0) — the base subset's own
|
||||
// diffuse alpha gates how much detail shows through, exactly like the base
|
||||
// pass's translucency-fade multiplier already does for mesh_modern. 1.0 for
|
||||
// every opaque subset; <1.0 while a TransparentPartHook fade is in flight.
|
||||
layout(std430, binding = 7) readonly buffer InstanceAlphaBuf {
|
||||
float instanceAlpha[];
|
||||
};
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
uniform int uDrawIDOffset;
|
||||
uniform uint uTextureIndexB; // absolute transform prefix in the shared pose arena
|
||||
uniform float uParamA; // detail UV tiling
|
||||
// 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;
|
||||
out float vDetailOpacity;
|
||||
out vec3 vWorldPos; // review fix: mesh_detail.frag needs this for applyFog,
|
||||
// exactly like mesh_modern.vert's vWorldPos.
|
||||
out flat uint vBaseTextureIndex;
|
||||
out flat uint vBaseTextureLayer;
|
||||
out flat uint vBatchFlags;
|
||||
out flat uint vDetailCategory;
|
||||
|
||||
void main() {
|
||||
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
int instanceIndex = transformIndex - int(uTextureIndexB);
|
||||
vec4 worldPos = Instances[transformIndex].transform * vec4(aPosition, 1.0);
|
||||
gl_Position = uViewProjection * worldPos;
|
||||
vWorldPos = worldPos.xyz;
|
||||
|
||||
// No distance term: VM2 found the fade only exists in
|
||||
// D3DPolyRender::DrawPolyInternal (0x0059d7c0, the immediate-polygon
|
||||
// path) and only when the static noFadeDetail (0x00820e38, initialised
|
||||
// to 1) is 0. Every loaded CGfxObj sets use_built_mesh=1
|
||||
// (CGfxObj::InitLoad 0x005346b0), so buildings/EnvCells never reach that
|
||||
// function; their attenuation is the LINEAR mip chain converging to the
|
||||
// texture mean, which the existing sampler already provides.
|
||||
vDetailOpacity = instanceAlpha[instanceIndex];
|
||||
vBaseUv = aTexCoord;
|
||||
vDetailUv = aTexCoord * uParamA;
|
||||
|
||||
BatchData batch = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vBaseTextureIndex = batch.textureIndex;
|
||||
vBaseTextureLayer = batch.textureLayer;
|
||||
vBatchFlags = batch.flags;
|
||||
vDetailCategory = uParamB == 1.0
|
||||
? instanceDetailCategory[instanceIndex]
|
||||
: 1u;
|
||||
}
|
||||
|
|
@ -12,6 +12,11 @@ in flat uint vTextureIndex;
|
|||
in flat uint vTextureLayer;
|
||||
in flat float vOpacityMultiplier; // #188
|
||||
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
||||
in flat float vSurfaceOpacity;
|
||||
in flat uint vBatchFlags;
|
||||
in flat uint vDetailCategory;
|
||||
|
||||
#include "retail_detail_material.glsl"
|
||||
|
||||
// uRenderPass values (Phase N.5 Decision 2 — two-pass alpha-test):
|
||||
// 0 = opaque pass — discard fragments with alpha < 0.95
|
||||
|
|
@ -26,6 +31,8 @@ uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3
|
|||
// 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;
|
||||
uniform uint uTextureIndexA;
|
||||
uniform float uParamA;
|
||||
|
||||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||||
struct Light {
|
||||
|
|
@ -70,35 +77,6 @@ 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) {
|
||||
// '<' 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.
|
||||
//
|
||||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): do NOT discard α≥0.95 here.
|
||||
// Native AC transparent-flagged surfaces routinely include
|
||||
// effectively-opaque pixels — e.g. the Holtburg lifestone crystal core
|
||||
// (surface 0x080011DE) which the spawn manifest classifies as
|
||||
// transparent (batch.IsTransparent=True) but whose decoded texture
|
||||
// alpha lands ≥0.95 across the visible surface. Those pixels still
|
||||
// compose correctly under (SrcAlpha, 1-SrcAlpha) alpha-blending, so
|
||||
// discarding them here threw away the whole crystal. The original
|
||||
// N.5 §2 rationale (high-α fragments belong in the opaque pass) does
|
||||
// not apply when the SURFACE is dat-flagged transparent — those
|
||||
// pixels can't reach the opaque pass at all.
|
||||
//
|
||||
// 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 < alphaCutoff) discard;
|
||||
}
|
||||
|
||||
// Per-vertex Gouraud lighting from the vertex shader (ambient + capped
|
||||
// lights), passed through retail CMaterial's two click-pulse replacements:
|
||||
// emissive/luminosity + diffuse * scene lighting. Normal is (0,1).
|
||||
|
|
@ -108,6 +86,7 @@ void main() {
|
|||
// ignored). Stripes visible HERE = a vertex-lighting artifact; absent =
|
||||
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
|
||||
if (uLightDebug == 3) {
|
||||
if (color.a < alphaCutoff) discard;
|
||||
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
|
||||
return;
|
||||
}
|
||||
|
|
@ -118,11 +97,32 @@ void main() {
|
|||
// Retail clamp per-channel to 1.0 (r13 §13.1).
|
||||
lit = min(lit, vec3(1.0));
|
||||
|
||||
vec3 rgb = color.rgb * lit;
|
||||
bool detailActive = uParamA != 0.0
|
||||
&& vDetailCategory != 0u
|
||||
&& (vBatchFlags & 1u) != 0u;
|
||||
vec3 rgb;
|
||||
float alpha;
|
||||
if (detailActive) {
|
||||
vec4 detail = ACDREAM_SAMPLE_ARRAY(
|
||||
uTextureIndexA,
|
||||
vec3(vTexCoord * uParamA, 0.0));
|
||||
RetailDetailMaterialResult material = acdreamRetailDetailMaterial(
|
||||
color.rgb,
|
||||
lit,
|
||||
detail,
|
||||
vSurfaceOpacity * vOpacityMultiplier);
|
||||
rgb = material.rgb;
|
||||
alpha = material.alpha;
|
||||
} else {
|
||||
rgb = color.rgb * lit;
|
||||
alpha = color.a * vOpacityMultiplier;
|
||||
}
|
||||
|
||||
// '<' 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)
|
||||
discard;
|
||||
|
||||
rgb = applyFog(rgb, vWorldPos);
|
||||
// #188: multiply the FINAL alpha only — the discard thresholds above stay
|
||||
// keyed on the raw sampled color.a, so the last few frames of a fade
|
||||
// (multiplier crossing under 0.05) still ramp smoothly toward zero rather
|
||||
// than popping invisible early against the discard cutoff.
|
||||
FragColor = vec4(rgb, color.a * vOpacityMultiplier);
|
||||
FragColor = vec4(rgb, alpha);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ struct InstanceData {
|
|||
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
||||
struct BatchData {
|
||||
uint textureIndex; // slot into the global texture table
|
||||
uint _pad; // keeps textureLayer/flags at offsets 8/12
|
||||
float surfaceOpacity; // authored material alpha (not base texture alpha)
|
||||
uint textureLayer; // layer in the shared WB or pooled composite array
|
||||
uint flags; // reserved — N.5 dispatcher owns all blend state
|
||||
// (glBlendFunc per pass). If a future phase wants
|
||||
|
|
@ -112,6 +112,10 @@ layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
|
|||
vec2 instanceSelectionLighting[];
|
||||
};
|
||||
|
||||
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
||||
uint instanceDetailCategory[];
|
||||
};
|
||||
|
||||
uniform mat4 uViewProjection;
|
||||
|
||||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
|
||||
|
|
@ -272,6 +276,9 @@ out flat uint vTextureIndex;
|
|||
out flat uint vTextureLayer;
|
||||
out flat float vOpacityMultiplier; // #188
|
||||
out flat vec2 vSelectionLighting;
|
||||
out flat float vSurfaceOpacity;
|
||||
out flat uint vBatchFlags;
|
||||
out flat uint vDetailCategory;
|
||||
|
||||
void main() {
|
||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||
|
|
@ -295,4 +302,7 @@ void main() {
|
|||
// site in mesh_modern.frag, which is the only form Vulkan can express.
|
||||
vTextureIndex = b.textureIndex;
|
||||
vTextureLayer = b.textureLayer;
|
||||
vSurfaceOpacity = b.surfaceOpacity;
|
||||
vBatchFlags = b.flags;
|
||||
vDetailCategory = instanceDetailCategory[instanceIndex];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
// D3DPolyRender::SetSurface @0x0059C4D0, retail single-pass detail stages.
|
||||
// Keep this one source shared by the ordinary and atmospheric mesh families.
|
||||
struct RetailDetailMaterialResult {
|
||||
vec3 rgb;
|
||||
float alpha;
|
||||
};
|
||||
|
||||
RetailDetailMaterialResult acdreamRetailDetailMaterial(
|
||||
vec3 baseRgb,
|
||||
vec3 diffuseRgb,
|
||||
vec4 detail,
|
||||
float materialAlpha)
|
||||
{
|
||||
float weight = materialAlpha * detail.a;
|
||||
return RetailDetailMaterialResult(
|
||||
detail.rgb * weight + (baseRgb * diffuseRgb) * (1.0 - weight),
|
||||
materialAlpha * detail.a * detail.a);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -215,28 +215,12 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "e552edb7ef42e9e32812824c075310f2d99eb5826e527db5c22b54890ef02088",
|
||||
"sourceSha256": "71950823770a69d610db03e0c54c645934e3b46a686c8abac85b52f2476afcfa",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "62b001a72080ea74bdaef0fb2a4ed92533feaeab76187ce4215813c6327432e7",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mesh_detail",
|
||||
"vulkanReady": true,
|
||||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "2d8b5b42f053a2d42e1ff44db3e331d5bea2d3f7dd36e20d813b7ae22c033181",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "a5285c51e8d6a1fa91f403f8489819595fa5a20abaaaccc969f220ea2c4eb335",
|
||||
"sourceSha256": "5eb058e92ab27f1f6c245401e05f78a278fe00696b5c77f36ae33f8428f83bf3",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
@ -247,12 +231,12 @@
|
|||
"stages": [
|
||||
{
|
||||
"stage": "vert",
|
||||
"sourceSha256": "dba51ce23f2b621683fc78db73f608a0c000479b8e827cd3ecc8731d46c6ee64",
|
||||
"sourceSha256": "c41448edcd258313f921f3b66c0884c8f78d4d592eef1abe0804c6cb2890c58f",
|
||||
"compiled": true
|
||||
},
|
||||
{
|
||||
"stage": "frag",
|
||||
"sourceSha256": "d6ae09de279e814b159e7c481d6b66f3cc37c741b3d8a249fbe2bc8156e52d4e",
|
||||
"sourceSha256": "396c382af73505bc2cac7a2db5878a63166730e47096a94630c35ad8376fe565",
|
||||
"compiled": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1094,11 +1094,8 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
|
|||
/// this method. This list's premise is the opaque-only, depth-test-and-
|
||||
/// write, no-blend pipeline pair the walk's ordered stream actually uses
|
||||
/// TODAY — not <c>StreamMark</c> itself: if any future ordered-stream
|
||||
/// stage draws with blending or a disabled depth write (for example the
|
||||
/// deferred building-detail overlay replay,
|
||||
/// <c>WbDrawDispatcher.DrawBuildingDetailRangeRhi</c>'s
|
||||
/// <c>RetailDetailTransparent</c> pass — not wired into this walk path
|
||||
/// today, OrderedStream.cs:46-52), that stage's <c>StreamMark</c> MUST
|
||||
/// stage draws with blending or a disabled depth write, that stage's
|
||||
/// <c>StreamMark</c> MUST
|
||||
/// become a flush point again; the contract gets amended, not bent.</para>
|
||||
/// </summary>
|
||||
internal void Replay(IGpuFrame frame, IGpuPassEncoder encoder)
|
||||
|
|
|
|||
|
|
@ -28,9 +28,6 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
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;
|
||||
|
||||
|
|
@ -46,11 +43,12 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
/// to the retail alpha queue for later replay.
|
||||
/// </summary>
|
||||
internal bool TransparentDetailEnabled =>
|
||||
RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _environmentDetail);
|
||||
RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _environmentDetail)
|
||||
&& _environmentDetail.Tiling != 0f;
|
||||
|
||||
/// <summary>
|
||||
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
||||
/// seven pipelines ARE this renderer's program, so there is no second step
|
||||
/// four pipelines ARE this renderer's program, so there is no second step
|
||||
/// and no <c>Shader</c> to hand in.
|
||||
/// </summary>
|
||||
internal EnvCellRenderer(
|
||||
|
|
@ -82,33 +80,6 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
scope.SampleCount);
|
||||
_additivePipeline = CreateShellPipeline(
|
||||
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
||||
_detailPipeline = CreateShellPipeline(
|
||||
device,
|
||||
"envcell-retail-detail",
|
||||
GpuBlendMode.RetailDetail,
|
||||
depthWrite: true,
|
||||
scope.SampleCount,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: false));
|
||||
_transparentDetailPipeline = CreateShellPipeline(
|
||||
device,
|
||||
"envcell-retail-detail-alpha",
|
||||
GpuBlendMode.RetailDetail,
|
||||
depthWrite: false,
|
||||
scope.SampleCount,
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +238,7 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
BindRingSection<int>(
|
||||
encoder, frame, GpuBindingModel.StorageInstanceLightSets,
|
||||
_lightSetData.AsSpan(0, uniqueInstanceCount * lightStride));
|
||||
BindEnvironmentDetailCategory(encoder, frame);
|
||||
BindEnvironmentDetailCategory(encoder, frame, uniqueInstanceCount);
|
||||
|
||||
// The frame-global sections, bound after this renderer's own binds
|
||||
// because those binds are what select the descriptor scope.
|
||||
|
|
@ -282,9 +253,7 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
MemoryMarshal.AsBytes(_commands.AsSpan(0, totalDraws)).CopyTo(commands.Data);
|
||||
IGpuBuffer commandBuffer = commands.Buffer;
|
||||
uint commandBase = commands.OffsetBytes;
|
||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_environmentDetail);
|
||||
bool detailEnabled = TransparentDetailEnabled;
|
||||
|
||||
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
||||
{
|
||||
|
|
@ -324,56 +293,14 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
pushConstants.TextureIndexA = detailEnabled
|
||||
? _environmentDetail.TextureSlot.Index
|
||||
: 0u;
|
||||
pushConstants.ParamA = detailEnabled
|
||||
? _environmentDetail.Tiling
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
|
||||
// Retail DrawMesh's two-pass fallback redraws each transparent
|
||||
// RenderMeshSubset immediately, before the next delayed-alpha
|
||||
// subset. Preserve that base/detail adjacency so another shell or
|
||||
// particle cannot be composited between the two contributions.
|
||||
if (renderPass == WbRenderPass.Transparent && detailEnabled)
|
||||
{
|
||||
int rangeEnd = drawRange.FirstCommand + drawRange.CommandCount;
|
||||
for (int command = drawRange.FirstCommand; command < rangeEnd; command++)
|
||||
{
|
||||
BindPipelineWithMesh(encoder, rangeBasePipeline, mesh);
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.RenderPass = isAdditive
|
||||
? (int)renderPass | 0x100
|
||||
: (int)renderPass;
|
||||
pushConstants.DrawIdOffset = command;
|
||||
pushConstants.TextureIndexA = 0;
|
||||
pushConstants.ParamA = 0f;
|
||||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
commandBase + (uint)(command * sizeof(DrawElementsIndirectCommand)),
|
||||
1,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
|
||||
BindPipelineWithMesh(
|
||||
encoder,
|
||||
isClip ? _clipDetailPipeline! : _transparentDetailPipeline!,
|
||||
mesh);
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = command;
|
||||
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _environmentDetail.Tiling;
|
||||
pushConstants.ParamB = isClip
|
||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||
: 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
commandBase + (uint)(command * sizeof(DrawElementsIndirectCommand)),
|
||||
1,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
|
||||
|
|
@ -381,39 +308,10 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
|
||||
// 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
|
||||
// both this and buildings, matching LScape::ChangeRegion.
|
||||
if (renderPass == WbRenderPass.Opaque
|
||||
&& detailEnabled)
|
||||
{
|
||||
BindPipelineWithMesh(encoder, _detailPipeline!, mesh);
|
||||
pushConstants.RenderPass = 0;
|
||||
pushConstants.TextureIndexA = _environmentDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _environmentDetail.Tiling;
|
||||
pushConstants.ParamB = 0f;
|
||||
|
||||
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
||||
{
|
||||
MdiDrawRange drawRange = _mdiDrawRanges[drawRangeIndex];
|
||||
CullMode cullMode = ResolveRetailCellShellCullMode(
|
||||
(CullMode)(drawRange.GroupIndex % CullGroupCount));
|
||||
SetCullMode(encoder, cullMode);
|
||||
pushConstants.DrawIdOffset = drawRange.FirstCommand;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
encoder.MultiDrawIndexedIndirect(
|
||||
commandBuffer,
|
||||
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
|
||||
(uint)drawRange.CommandCount,
|
||||
(uint)sizeof(DrawElementsIndirectCommand));
|
||||
}
|
||||
}
|
||||
pushConstants.TextureIndexA = 0;
|
||||
pushConstants.ParamA = 0f;
|
||||
pushConstants.ParamB = 0f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
}
|
||||
|
||||
private void BindPipelineWithMesh(
|
||||
|
|
@ -502,23 +400,23 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds one category word for <c>mesh_detail.vert</c>'s statically used
|
||||
/// 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.
|
||||
/// Binds one EnvCell category word per instance for the world vertex
|
||||
/// shader's statically used binding 9. Vulkan requires the declared
|
||||
/// descriptor to be valid even when detail is unarmed.
|
||||
/// </summary>
|
||||
internal static void BindEnvironmentDetailCategory(
|
||||
internal void BindEnvironmentDetailCategory(
|
||||
IGpuPassEncoder encoder,
|
||||
IGpuFrame frame)
|
||||
IGpuFrame frame,
|
||||
int instanceCount)
|
||||
{
|
||||
Span<uint> category = stackalloc uint[1];
|
||||
category[0] = 1u;
|
||||
if (_detailCategoryData.Length < instanceCount)
|
||||
Array.Resize(ref _detailCategoryData, Math.Max(instanceCount, 16));
|
||||
Array.Fill(_detailCategoryData, 1u, 0, instanceCount);
|
||||
BindRingSection<uint>(
|
||||
encoder,
|
||||
frame,
|
||||
GpuBindingModel.StorageInstanceDetailCategory,
|
||||
category);
|
||||
_detailCategoryData.AsSpan(0, instanceCount));
|
||||
}
|
||||
|
||||
private void DisposeRhiResources()
|
||||
|
|
@ -531,11 +429,5 @@ public sealed unsafe partial class EnvCellRenderer
|
|||
_clipPipeline = null;
|
||||
_additivePipeline?.Dispose();
|
||||
_additivePipeline = null;
|
||||
_detailPipeline?.Dispose();
|
||||
_detailPipeline = null;
|
||||
_transparentDetailPipeline?.Dispose();
|
||||
_transparentDetailPipeline = null;
|
||||
_clipDetailPipeline?.Dispose();
|
||||
_clipDetailPipeline = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ public sealed partial class EnvCellRenderer :
|
|||
private uint[] _clipSlotData = Array.Empty<uint>();
|
||||
|
||||
// Campaign VM VM1 follow-up: per-instance opacity multiplier, parallel to
|
||||
// _gpuInstanceTransforms, feeding mesh_modern.vert/mesh_detail.vert's
|
||||
// _gpuInstanceTransforms, feeding both world vertex families'
|
||||
// InstanceAlphaBuf (binding 7, GpuBindingModel.StorageInstanceAlpha).
|
||||
// Before this field the shell and interior-detail passes never bound that
|
||||
// storage buffer at all, so both shaders read whatever section
|
||||
// Before this field the shell pass never bound that storage buffer at
|
||||
// all, so the shader read whatever section
|
||||
// WbDrawDispatcher's own SubmitRhi last bound in the same pass — an
|
||||
// unrelated object's opacity array, indexed by these cell instance ids.
|
||||
// EnvCell shells never carry a #188 TransparentPartHook translucency fade
|
||||
|
|
@ -137,6 +137,7 @@ public sealed partial class EnvCellRenderer :
|
|||
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
|
||||
private ModernBatchData[] _modernBatches = Array.Empty<ModernBatchData>();
|
||||
private uint[] _detailCategoryData = Array.Empty<uint>();
|
||||
private readonly List<EnvCellLandblock> _prepareLandblocks = new();
|
||||
private readonly List<InstanceData> _renderInstances = new();
|
||||
private readonly List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> _renderDrawCalls = new();
|
||||
|
|
@ -882,7 +883,7 @@ public sealed partial class EnvCellRenderer :
|
|||
EnvCellTransparentRoute transparentRoute,
|
||||
bool detailSurfaceActive)
|
||||
{
|
||||
// WB EnvCellRenderManager.cs:400: the RHI arm's three pipelines are built
|
||||
// WB EnvCellRenderManager.cs:400: the RHI arm's four pipelines are built
|
||||
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
||||
// answers whether this renderer is ready to draw.
|
||||
if (!_initialized) return;
|
||||
|
|
@ -1312,6 +1313,7 @@ public sealed partial class EnvCellRenderer :
|
|||
// Slice V4t: the slot is the device's, assigned by the
|
||||
// mesh manager at upload rather than interned here.
|
||||
TextureTableIndex = item.batch.TextureSlot.Index,
|
||||
SurfaceOpacity = item.batch.SurfaceOpacity,
|
||||
TextureIndex = (uint)item.batch.TextureIndex,
|
||||
// #226: this renderer submits built EnvCell meshes.
|
||||
// Retail DrawMesh forwards curr_detail_surface to
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// <summary>
|
||||
/// Campaign VM VM6: the two <c>BatchData.flags</c> bits that mark a world
|
||||
/// draw batch as swaying foliage — bit 0 remains #226's built-mesh marker
|
||||
/// (see <c>mesh_detail.frag</c>'s <c>vBatchFlags & 1u</c> check) and is
|
||||
/// (see the world fragment shaders' <c>vBatchFlags & 1u</c> check) and is
|
||||
/// never touched here. <c>mesh_atmospheric.vert</c> and the four
|
||||
/// <c>directional_shadow_world_*.vert</c> caster shaders read these bits
|
||||
/// through the shared <c>foliage_wind.glsl</c> include
|
||||
|
|
|
|||
|
|
@ -52,4 +52,5 @@ internal readonly record struct GroupKey(
|
|||
uint TextureLayer,
|
||||
TranslucencyKind Translucency,
|
||||
uint FoliageFlags,
|
||||
float SurfaceOpacity = 1f,
|
||||
CullMode CullMode = CullMode.CounterClockwise);
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ namespace AcDream.App.Rendering.Wb {
|
|||
/// Consists of a texture-table slot (Campaign V slice V2; was a raw 64-bit
|
||||
/// bindless handle) and the layer index within the shared/pooled array.
|
||||
/// Indexed by gl_DrawIDARB in the vertex shader. Same 16-byte std430 shape
|
||||
/// as mesh_modern.vert's BatchData: TextureTableIndex/Reserved/TextureIndex/
|
||||
/// as mesh_modern.vert's BatchData: TextureTableIndex/SurfaceOpacity/TextureIndex/
|
||||
/// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct ModernBatchData {
|
||||
public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table
|
||||
public uint Reserved; // 4 bytes — pad, keeps TextureIndex/Flags at offsets 8/12
|
||||
public float SurfaceOpacity; // 4 bytes — authored material alpha
|
||||
public uint TextureIndex; // 4 bytes — layer within the texture array
|
||||
public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@ namespace AcDream.App.Rendering.Wb
|
|||
public bool IsTransparent { get; set; }
|
||||
public bool IsAdditive { get; set; }
|
||||
public bool HasWrappingUVs { get; set; }
|
||||
/// <summary>Authored material alpha; base texture alpha is separate.</summary>
|
||||
public float SurfaceOpacity { get; set; } = 1f;
|
||||
|
||||
// Modern rendering path fields
|
||||
public uint FirstIndex { get; set; }
|
||||
|
|
@ -2263,6 +2265,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
IsTransparent = batch.IsTransparent,
|
||||
IsAdditive = batch.IsAdditive,
|
||||
HasWrappingUVs = batch.HasWrappingUVs,
|
||||
SurfaceOpacity = batch.SurfaceOpacity,
|
||||
Key = batch.Key,
|
||||
CullMode = batch.CullMode,
|
||||
FirstIndex = firstIndex,
|
||||
|
|
|
|||
|
|
@ -42,14 +42,9 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// calls the pair (prepare once, draw the whole stream as one range, or as
|
||||
/// several — see <c>OrderPreservingSubmitterTests</c>).</para>
|
||||
///
|
||||
/// <para>Scope: this stage proves walk-order submission through the existing
|
||||
/// RHI on static content (plan §FW2). It does NOT wire the retail
|
||||
/// building-detail overlay replay (<c>DrawBuildingDetailRangeRhi</c>'s second
|
||||
/// pass through <c>RetailDetail</c>/<c>RetailDetailTransparent</c>) — a
|
||||
/// detail-category command still forces a solo merge run (mirroring the
|
||||
/// deferred-alpha detail break), but this submitter issues only the
|
||||
/// base-pipeline draw for it. The overlay replay is production wiring,
|
||||
/// deferred to whichever stage cuts the walk over for real content.</para>
|
||||
/// <para>OVERHAUL S5-c4 arms retail detail on a detail-category command's
|
||||
/// existing solo base run. It remains one draw and therefore introduces no
|
||||
/// blended overlay or additional terrain/StreamMark flush boundary.</para>
|
||||
/// </summary>
|
||||
public sealed unsafe partial class WbDrawDispatcher
|
||||
{
|
||||
|
|
@ -395,6 +390,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
_batchData[i] = new BatchData
|
||||
{
|
||||
TextureIndex = key.TextureSlot.Index,
|
||||
SurfaceOpacity = key.SurfaceOpacity,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Flags = 1u | key.FoliageFlags,
|
||||
};
|
||||
|
|
@ -545,6 +541,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
IGpuBuffer commandBuffer = _orderedCommands.Buffer!;
|
||||
uint commandBase = _orderedCommands.OffsetBytes;
|
||||
int rangeEnd = firstCommand + commandCount;
|
||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_buildingDetail)
|
||||
&& _buildingDetail.Tiling != 0f;
|
||||
|
||||
foreach (OrderedMergeRun run in _orderedRuns)
|
||||
{
|
||||
|
|
@ -569,6 +569,11 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
PipelineBucket bucket = BucketFor(_orderedStream.Keys[run.FirstCommand].Translucency);
|
||||
IGpuPipeline pipeline = PipelineForBucket(pipelines, bucket);
|
||||
pushConstants.RenderPass = bucket == PipelineBucket.Opaque ? 0 : 1;
|
||||
if (detailEnabled
|
||||
&& _orderedStream.DetailCategories[run.FirstCommand] != 0u)
|
||||
ArmBuildingDetail(ref pushConstants);
|
||||
else
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
|
||||
BindPipelineWithMesh(encoder, pipeline, global);
|
||||
DrawIndirectRangeRhi(
|
||||
|
|
@ -580,6 +585,9 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
run.FirstCommand,
|
||||
run.CommandCount);
|
||||
}
|
||||
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
private readonly IWorldPassScope? _scope;
|
||||
|
||||
/// <summary>
|
||||
/// The seven mesh pipelines at ONE sample count.
|
||||
/// The five retail material/blend pipelines at ONE sample count.
|
||||
///
|
||||
/// <para>Campaign V slice V6l: there are two of these. Vulkan requires a
|
||||
/// pipeline's <c>rasterizationSamples</c> to equal the pass it draws in, and
|
||||
|
|
@ -52,16 +52,13 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
IGpuPipeline OpaqueAlphaToCoverage,
|
||||
IGpuPipeline AlphaBlend,
|
||||
IGpuPipeline AlphaAdditive,
|
||||
IGpuPipeline AlphaInverse,
|
||||
IGpuPipeline RetailDetail,
|
||||
IGpuPipeline RetailDetailTransparent);
|
||||
IGpuPipeline AlphaInverse);
|
||||
|
||||
private MeshPipelineSet? _backbufferPipelines;
|
||||
private MeshPipelineSet? _offscreenPipelines;
|
||||
|
||||
private const string OpaqueTimerScope = "wb-entities-opaque";
|
||||
private const string TransparentTimerScope = "wb-entities-transparent";
|
||||
private const string DetailTimerScope = "wb-buildings-detail";
|
||||
|
||||
private readonly TerrainAtlas.RetailDetailTextureBinding _buildingDetail;
|
||||
private readonly Func<bool> _buildingDetailEnabled;
|
||||
|
|
@ -158,8 +155,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
|
||||
/// <summary>
|
||||
/// The RHI arm's constructor. No GL context, no <c>Shader</c>, no
|
||||
/// <c>BindlessSupport</c>: five base pipelines compile <c>mesh_modern</c> and
|
||||
/// the detail overlay compiles <c>mesh_detail</c> from committed SPIR-V;
|
||||
/// <c>BindlessSupport</c>: five base pipelines compile <c>mesh_modern</c>;
|
||||
/// batch data already carries the device's own
|
||||
/// <c>GpuTextureSlot</c> (V4t) rather than a bindless handle.
|
||||
/// </summary>
|
||||
|
|
@ -231,7 +227,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
bool usesRenderPackShaderAbi = false)
|
||||
{
|
||||
string suffix = samples > 1 ? string.Empty : "-1x";
|
||||
var created = new List<IGpuPipeline>(7);
|
||||
var created = new List<IGpuPipeline>(5);
|
||||
try
|
||||
{
|
||||
return new MeshPipelineSet(
|
||||
|
|
@ -260,28 +256,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples,
|
||||
shaders: baseShaders,
|
||||
shaderName: baseShaderName,
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||
Track(CreateMeshPipeline(
|
||||
device,
|
||||
$"wb-mesh-retail-detail{suffix}",
|
||||
GpuBlendMode.RetailDetail,
|
||||
true,
|
||||
false,
|
||||
samples,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: false),
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
||||
Track(CreateMeshPipeline(
|
||||
device,
|
||||
$"wb-mesh-retail-detail-alpha{suffix}",
|
||||
GpuBlendMode.RetailDetail,
|
||||
false,
|
||||
false,
|
||||
samples,
|
||||
shaderName: "mesh_detail",
|
||||
depthCompare: RetailDetailTextureContract.DetailDepthCompare(
|
||||
transparent: true),
|
||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)));
|
||||
}
|
||||
catch
|
||||
|
|
@ -441,10 +415,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
transformBaseInstance);
|
||||
IGpuBuffer commandBuffer = commands.Buffer;
|
||||
uint commandBase = commands.OffsetBytes;
|
||||
ReadOnlySpan<DrawElementsIndirectCommand> usedCommands =
|
||||
_indirectCommands.AsSpan(0, totalDraws);
|
||||
ReadOnlySpan<uint> usedDetailCategories =
|
||||
_detailCategoryData.AsSpan(0, immediateInstances);
|
||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_buildingDetail)
|
||||
&& _buildingDetail.Tiling != 0f;
|
||||
|
||||
// ── Phase 7: opaque pass ─────────────────────────────────────────────
|
||||
if (_opaqueDrawCount > 0)
|
||||
|
|
@ -459,67 +435,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
encoder.SetPushConstants(in pushConstants);
|
||||
using (BeginRhiTimer(encoder, diag, OpaqueTimerScope))
|
||||
{
|
||||
DrawIndirectRangeRhi(
|
||||
DrawDetailAwareRangeRhi(
|
||||
encoder, ref pushConstants, commandBuffer, commandBase,
|
||||
0, _opaqueDrawCount);
|
||||
0, _opaqueDrawCount, usedDetailCategories, detailEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
// Retail DrawBuilding detail category (1). Only consecutive command
|
||||
// runs containing a building are replayed; mesh_detail filters ordinary
|
||||
// instances inside a mixed command. This includes ClipMap built-mesh
|
||||
// subsets: the named retail
|
||||
// DrawMesh path forwards curr_detail_surface to RenderMeshSubset for
|
||||
// every material kind. The setting is read at draw time so the existing
|
||||
// Options checkbox changes the live scene immediately.
|
||||
if (_opaqueDrawCount > 0
|
||||
&& RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_buildingDetail))
|
||||
{
|
||||
int searchStart = 0;
|
||||
if (TryGetNextDetailCommandRun(
|
||||
usedCommands,
|
||||
usedDetailCategories,
|
||||
searchStart,
|
||||
_opaqueDrawCount,
|
||||
out DetailCommandRun run))
|
||||
{
|
||||
BindPipelineWithMesh(encoder, pipelines.RetailDetail, mesh);
|
||||
pushConstants.RenderPass = 0;
|
||||
pushConstants.DrawIdOffset = 0;
|
||||
pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _buildingDetail.Tiling;
|
||||
pushConstants.ParamB = 1f;
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
using (BeginRhiTimer(encoder, diag, DetailTimerScope))
|
||||
{
|
||||
do
|
||||
{
|
||||
DrawIndirectRangeRhi(
|
||||
encoder,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
run.FirstCommand,
|
||||
run.CommandCount);
|
||||
searchStart = run.FirstCommand + run.CommandCount;
|
||||
}
|
||||
while (TryGetNextDetailCommandRun(
|
||||
usedCommands,
|
||||
usedDetailCategories,
|
||||
searchStart,
|
||||
_opaqueDrawCount,
|
||||
out run));
|
||||
}
|
||||
}
|
||||
|
||||
// Later base passes expect neutral spare push fields.
|
||||
pushConstants.TextureIndexA = 0;
|
||||
pushConstants.ParamA = 0f;
|
||||
pushConstants.ParamB = 0f;
|
||||
}
|
||||
|
||||
// ── Phase 8: transparent pass ────────────────────────────────────────
|
||||
if (_transparentDrawCount > 0)
|
||||
{
|
||||
|
|
@ -538,7 +459,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
usedDetailCategories);
|
||||
usedDetailCategories,
|
||||
detailEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -594,7 +516,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
/// difference between the two call sites is <paramref name="viewProjection"/>
|
||||
/// (the drain reads the deferred scope's own <c>_deferredAlphaViewProjection</c>;
|
||||
/// an immediate draw uses whatever the CURRENT frame's viewProjection
|
||||
/// is) and whether a building-detail overlay pass follows.
|
||||
/// is). The immediate site additionally arms detail on its original draw.
|
||||
/// </summary>
|
||||
private GpuPushConstants BindAlphaDrawState(
|
||||
IGpuPassEncoder encoder,
|
||||
|
|
@ -703,14 +625,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
/// <summary>
|
||||
/// S4-c2 fix round 1 (M3b): <c>DrawMesh</c> row 1's "render immediately;
|
||||
/// enqueue nothing" outcome for ONE translucent building-shell instance
|
||||
/// under building detail — bind the blend pipeline, one indirect draw,
|
||||
/// then the detail pass, reusing <see cref="BindAlphaDrawState"/> and
|
||||
/// <see cref="DrawBuildingDetailRangeRhi"/> (the same code
|
||||
/// <see cref="DrawPreparedAlphaBatchRhi"/> used to use) for the single
|
||||
/// slot <see cref="WbDrawDispatcher.DrawImmediateAlphaInstance"/> just
|
||||
/// under building detail — arm the retail one-pass material on the
|
||||
/// original blend pipeline and submit the single slot
|
||||
/// <see cref="WbDrawDispatcher.DrawImmediateAlphaInstance"/> just
|
||||
/// prepared. The caller only reaches this method when
|
||||
/// <c>detailSurfaceActive</c> was true, so the detail pass always
|
||||
/// applies here — the drain's own per-command detail CHECK is gone
|
||||
/// <c>detailSurfaceActive</c> was true, so detail always applies here —
|
||||
/// the drain's own per-command detail CHECK is gone
|
||||
/// (M3c) precisely because detail-eligible content no longer reaches it.
|
||||
/// </summary>
|
||||
private void DrawImmediateAlphaInstanceRhi(
|
||||
|
|
@ -726,6 +646,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
|
||||
BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
ArmBuildingDetail(ref pushConstants);
|
||||
DrawIndirectRangeRhi(
|
||||
encoder,
|
||||
ref pushConstants,
|
||||
|
|
@ -733,16 +654,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
_alphaCommands.OffsetBytes,
|
||||
startCommand: 0,
|
||||
commandCount: 1);
|
||||
|
||||
DrawBuildingDetailRangeRhi(
|
||||
encoder,
|
||||
mesh,
|
||||
pipelines.RetailDetailTransparent,
|
||||
ref pushConstants,
|
||||
_alphaCommands.Buffer!,
|
||||
_alphaCommands.OffsetBytes,
|
||||
firstCommand: 0,
|
||||
commandCount: 1);
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
}
|
||||
|
||||
private void DrawImmediateTransparentRhi(
|
||||
|
|
@ -752,21 +665,21 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
ref GpuPushConstants pushConstants,
|
||||
IGpuBuffer commandBuffer,
|
||||
uint commandBase,
|
||||
ReadOnlySpan<uint> usedDetailCategories)
|
||||
ReadOnlySpan<uint> usedDetailCategories,
|
||||
bool detailEnabled)
|
||||
{
|
||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||
_buildingDetailEnabled(),
|
||||
_buildingDetail);
|
||||
int command = _opaqueDrawCount;
|
||||
int end = command + _transparentDrawCount;
|
||||
while (command < end)
|
||||
{
|
||||
TranslucencyKind blend = _groupInputScratch[command].Translucency;
|
||||
bool hasDetail = detailEnabled
|
||||
&& CommandContainsDetailCategory(
|
||||
_indirectCommands[command],
|
||||
usedDetailCategories);
|
||||
int runEnd = command + 1;
|
||||
while (runEnd < end
|
||||
&& _groupInputScratch[runEnd].Translucency == blend
|
||||
&& !hasDetail
|
||||
&& (!detailEnabled
|
||||
|| !CommandContainsDetailCategory(
|
||||
|
|
@ -776,8 +689,14 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
runEnd++;
|
||||
}
|
||||
|
||||
BindPipelineWithMesh(encoder, pipelines.AlphaBlend, mesh);
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
BindPipelineWithMesh(
|
||||
encoder,
|
||||
PipelineForBlend(pipelines, blend),
|
||||
mesh);
|
||||
if (hasDetail)
|
||||
ArmBuildingDetail(ref pushConstants);
|
||||
else
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
DrawIndirectRangeRhi(
|
||||
encoder,
|
||||
ref pushConstants,
|
||||
|
|
@ -785,45 +704,60 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
commandBase,
|
||||
command,
|
||||
runEnd - command);
|
||||
|
||||
if (hasDetail)
|
||||
{
|
||||
DrawBuildingDetailRangeRhi(
|
||||
encoder,
|
||||
mesh,
|
||||
pipelines.RetailDetailTransparent,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
command,
|
||||
1);
|
||||
}
|
||||
command = runEnd;
|
||||
}
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
}
|
||||
|
||||
private void DrawBuildingDetailRangeRhi(
|
||||
private void DrawDetailAwareRangeRhi(
|
||||
IGpuPassEncoder encoder,
|
||||
GlobalMeshBuffer mesh,
|
||||
IGpuPipeline detailPipeline,
|
||||
ref GpuPushConstants pushConstants,
|
||||
IGpuBuffer commandBuffer,
|
||||
uint commandBase,
|
||||
int firstCommand,
|
||||
int commandCount)
|
||||
int commandCount,
|
||||
ReadOnlySpan<uint> detailCategories,
|
||||
bool detailEnabled)
|
||||
{
|
||||
int command = firstCommand;
|
||||
int end = firstCommand + commandCount;
|
||||
while (command < end)
|
||||
{
|
||||
bool hasDetail = detailEnabled
|
||||
&& CommandContainsDetailCategory(
|
||||
_indirectCommands[command], detailCategories);
|
||||
int runEnd = command + 1;
|
||||
while (runEnd < end
|
||||
&& !hasDetail
|
||||
&& (!detailEnabled
|
||||
|| !CommandContainsDetailCategory(
|
||||
_indirectCommands[runEnd], detailCategories)))
|
||||
{
|
||||
runEnd++;
|
||||
}
|
||||
|
||||
if (hasDetail)
|
||||
ArmBuildingDetail(ref pushConstants);
|
||||
else
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
DrawIndirectRangeRhi(
|
||||
encoder,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
command,
|
||||
runEnd - command);
|
||||
command = runEnd;
|
||||
}
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
encoder.SetPushConstants(in pushConstants);
|
||||
}
|
||||
|
||||
private void ArmBuildingDetail(ref GpuPushConstants pushConstants)
|
||||
{
|
||||
BindPipelineWithMesh(encoder, detailPipeline, mesh);
|
||||
pushConstants.TextureIndexA = _buildingDetail.TextureSlot.Index;
|
||||
pushConstants.ParamA = _buildingDetail.Tiling;
|
||||
pushConstants.ParamB = 1f;
|
||||
DrawIndirectRangeRhi(
|
||||
encoder,
|
||||
ref pushConstants,
|
||||
commandBuffer,
|
||||
commandBase,
|
||||
firstCommand,
|
||||
commandCount);
|
||||
ClearDetailPushConstants(ref pushConstants);
|
||||
}
|
||||
|
||||
private static void ClearDetailPushConstants(
|
||||
|
|
@ -1139,11 +1073,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
totalMs += transparentMs;
|
||||
any = true;
|
||||
}
|
||||
if (_device.Timers.TryResolve(DetailTimerScope, out double detailMs))
|
||||
{
|
||||
totalMs += detailMs;
|
||||
any = true;
|
||||
}
|
||||
if (!any)
|
||||
return;
|
||||
|
||||
|
|
@ -1174,8 +1103,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
|||
pipelines.AlphaBlend.Dispose();
|
||||
pipelines.AlphaAdditive.Dispose();
|
||||
pipelines.AlphaInverse.Dispose();
|
||||
pipelines.RetailDetail.Dispose();
|
||||
pipelines.RetailDetailTransparent.Dispose();
|
||||
}
|
||||
|
||||
private sealed class NullRhiTimerScope : IDisposable
|
||||
|
|
|
|||
|
|
@ -241,6 +241,7 @@ public sealed partial class WbDrawDispatcher
|
|||
batch.FirstIndex, (int)batch.BaseVertex,
|
||||
batch.IndexCount, texture.Slot, texture.Layer, translucency,
|
||||
FoliageFlags: foliageFlags,
|
||||
SurfaceOpacity: batch.SurfaceOpacity,
|
||||
CullMode: batch.CullMode);
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -617,8 +617,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
private int _transparentDrawCount;
|
||||
private int _transparentByteOffset;
|
||||
|
||||
// Campaign V slice V2 (2026-07-27): std430 layout: uint TextureIndex at
|
||||
// offset 0, uint Reserved (pad) at offset 4, uint TextureLayer at offset 8,
|
||||
// std430 layout: uint TextureIndex at offset 0, float SurfaceOpacity at
|
||||
// offset 4, uint TextureLayer at offset 8,
|
||||
// uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so
|
||||
// every existing CPU writer's offsets are unchanged (see
|
||||
// GpuBindingModel.GpuBatchDataStrideBytes). TextureIndex used to be a
|
||||
|
|
@ -630,7 +630,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
private struct BatchData
|
||||
{
|
||||
public uint TextureIndex; // slot into the device texture table
|
||||
public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12
|
||||
public float SurfaceOpacity;
|
||||
public uint TextureLayer;
|
||||
public uint Flags;
|
||||
}
|
||||
|
|
@ -1947,6 +1947,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
_batchData[i] = new BatchData
|
||||
{
|
||||
TextureIndex = _batchPublicScratch[i].TextureIndex,
|
||||
SurfaceOpacity = _batchPublicScratch[i].SurfaceOpacity,
|
||||
TextureLayer = _batchPublicScratch[i].TextureLayer,
|
||||
Flags = _batchPublicScratch[i].Flags,
|
||||
};
|
||||
|
|
@ -2049,6 +2050,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
TextureIndex: g.TextureSlot.Index,
|
||||
TextureLayer: g.TextureLayer,
|
||||
Translucency: g.Translucency,
|
||||
SurfaceOpacity: g.SurfaceOpacity,
|
||||
CullMode: g.CullMode,
|
||||
FoliageFlags: g.FoliageFlags);
|
||||
|
||||
|
|
@ -2122,6 +2124,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
g.TextureLayer,
|
||||
g.Translucency,
|
||||
g.FoliageFlags,
|
||||
g.SurfaceOpacity,
|
||||
g.CullMode);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2143,6 +2146,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
Translucency = key.Translucency,
|
||||
CullMode = key.CullMode,
|
||||
FoliageFlags = key.FoliageFlags,
|
||||
SurfaceOpacity = key.SurfaceOpacity,
|
||||
Registration = registration,
|
||||
LastUsedFrame = frame,
|
||||
};
|
||||
|
|
@ -2478,7 +2482,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
byte mask = RetailAlphaMeshRouter.MaskFromTranslucencyKind(kind);
|
||||
bool detailSurfaceActive = isBuildingShell
|
||||
&& RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _buildingDetail);
|
||||
&& RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _buildingDetail)
|
||||
&& _buildingDetail.Tiling != 0f;
|
||||
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
||||
currentlyDrawingSky: false,
|
||||
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
||||
|
|
@ -2558,11 +2563,12 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
// Campaign V slice V2: table slot, not the raw handle.
|
||||
TextureIndex = key.TextureSlot.Index,
|
||||
SurfaceOpacity = key.SurfaceOpacity,
|
||||
TextureLayer = key.TextureLayer,
|
||||
// DrawMesh invokes RenderMeshSubset with detail enabled for
|
||||
// every built-mesh material subset while curr_detail_surface
|
||||
// is installed. The per-instance category still rejects
|
||||
// ordinary objects in mesh_detail. Review fix round 2 (F2):
|
||||
// ordinary objects in the one-pass fragment arm. Review fix round 2 (F2):
|
||||
// bit 0 (the #226 built-mesh marker) is unconditional here —
|
||||
// every delayed-alpha entry IS a built-mesh alpha submission
|
||||
// — but the previous hardcoded `1` silently dropped bits 1/2
|
||||
|
|
@ -3431,6 +3437,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
uint TextureIndex,
|
||||
uint TextureLayer,
|
||||
TranslucencyKind Translucency,
|
||||
float SurfaceOpacity = 1f,
|
||||
CullMode CullMode = CullMode.CounterClockwise,
|
||||
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
|
||||
// TrunkFlag, OR'd into BatchDataPublic.Flags alongside #226's bit 0
|
||||
|
|
@ -3445,7 +3452,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
public struct BatchDataPublic
|
||||
{
|
||||
public uint TextureIndex;
|
||||
public uint Reserved;
|
||||
public float SurfaceOpacity;
|
||||
public uint TextureLayer;
|
||||
public uint Flags;
|
||||
}
|
||||
|
|
@ -3496,7 +3503,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
var bd = new BatchDataPublic
|
||||
{
|
||||
TextureIndex = g.TextureIndex,
|
||||
Reserved = 0,
|
||||
SurfaceOpacity = g.SurfaceOpacity,
|
||||
TextureLayer = g.TextureLayer,
|
||||
// #226: this is the built-mesh path. Retail DrawMesh passes
|
||||
// curr_detail_surface through RenderMeshSubset for opaque,
|
||||
|
|
@ -3668,6 +3675,7 @@ 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 float SurfaceOpacity = 1f;
|
||||
public CullMode CullMode;
|
||||
public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes)
|
||||
public int InstanceCount;
|
||||
|
|
@ -3717,7 +3725,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
|||
public readonly List<uint> IndoorFlags = new();
|
||||
|
||||
// #226: 1 for a building-shell instance, 0 otherwise. Parallel to
|
||||
// Matrices and uploaded at storage binding 9 for the detail replay.
|
||||
// Matrices and uploaded at storage binding 9 for one-pass eligibility.
|
||||
public readonly List<uint> DetailCategories = new();
|
||||
|
||||
// #188: per-instance opacity multiplier, parallel to Matrices.
|
||||
|
|
|
|||
|
|
@ -32,10 +32,8 @@ namespace AcDream.App.Rendering;
|
|||
///
|
||||
/// <para>Scope: WORLD-SPACE geometry only (terrain, EnvCell shells, entity
|
||||
/// meshes, particles, portal punches, the directional shadow caster/receiver
|
||||
/// pair). The two <c>RetailDetail</c> pipelines
|
||||
/// (<see cref="RetailDetailTextureContract"/>'s <c>Equal</c>/<c>LessOrEqual</c>
|
||||
/// pair, used by the building-detail overlay replay) are a documented
|
||||
/// exception with their own citation and are untouched by this contract.</para>
|
||||
/// pair). Detail-active subsets retain those same base pipeline depth states;
|
||||
/// their material combine occurs inside the original fragment shader.</para>
|
||||
/// </summary>
|
||||
internal static class WorldDepthContract
|
||||
{
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ internal interface IWorldPassSurface
|
|||
/// world pass now: <c>portal_depth.vert</c> (the exit seals and punch
|
||||
/// fans, planes handed per draw through the TerrainClip UBO). S3 review
|
||||
/// fix round 1 (F5) deleted the mesh/particle vertex shaders'
|
||||
/// (<c>mesh_modern</c>, <c>mesh_atmospheric</c>, <c>mesh_detail</c>,
|
||||
/// (<c>mesh_modern</c>, <c>mesh_atmospheric</c>,
|
||||
/// <c>particle</c>, <c>particle_mesh</c>) own per-cell fetch and
|
||||
/// <c>gl_ClipDistance</c> loop outright: the per-cell clip-region table
|
||||
/// <see cref="PrepareClipFrame"/> used to publish for them had no path
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Distance are LIVE through S5-c3's shared building-degrade owner. Landscape
|
||||
/// Draw Distance is LIVE through <c>ApplyLandscapeDrawDistance</c>. Building
|
||||
/// Detail Textures is LIVE: #226 consumes it directly in the retail
|
||||
/// building/EnvCell detail pass;
|
||||
/// one-pass building/EnvCell detail material;
|
||||
/// Camera Stiffness/Adjustment Speed/Align To Slope/Mouse Look
|
||||
/// Sensitivity/Invert Mouselook Y Axis/Use Mouse Turning (TS-74 — no
|
||||
/// persistent mouse-turning camera mode exists for ANY of these six to
|
||||
|
|
@ -1294,7 +1294,7 @@ public static class ConfigOptionsPageController
|
|||
|
||||
// Every row except Building Detail Textures is still store-only:
|
||||
// #226 consumes that existing preference in the retail building and
|
||||
// EnvCell detail replay. The other rows remain aggregate-preset gaps.
|
||||
// EnvCell one-pass detail material. The other rows remain aggregate-preset gaps.
|
||||
BuildMenuRow(
|
||||
listBox, "ID_Graphics_LandscapeTextureDetail", TextureDetailChoices, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().LandscapeTextureDetail,
|
||||
|
|
@ -1336,7 +1336,7 @@ public static class ConfigOptionsPageController
|
|||
listBox, "ID_Graphics_BuildingDetailTextures", defaultValue: true, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().BuildingDetailTextures,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { BuildingDetailTextures = value }),
|
||||
storeOnly: false); // LIVE — #226 retail building/EnvCell detail pass
|
||||
storeOnly: false); // LIVE — #226 retail building/EnvCell detail material
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Graphics_MultiPassAlpha", defaultValue: false, page, resolveString,
|
||||
|
|
|
|||
|
|
@ -599,6 +599,10 @@ public sealed class MeshExtractor {
|
|||
Translucency =
|
||||
TranslucencyKindExtensions.FromSurfaceType(
|
||||
surface.Type),
|
||||
SurfaceOpacity =
|
||||
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
|
||||
surface.Type,
|
||||
surface.Translucency),
|
||||
IsTransparent = isTransparent,
|
||||
IsAdditive = isAdditive
|
||||
};
|
||||
|
|
@ -1072,6 +1076,10 @@ public sealed class MeshExtractor {
|
|||
UploadPixelFormat = uploadPixelFormat,
|
||||
UploadPixelType = uploadPixelType,
|
||||
Translucency = TranslucencyKindExtensions.FromSurfaceType(surface.Type),
|
||||
SurfaceOpacity =
|
||||
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
|
||||
surface.Type,
|
||||
surface.Translucency),
|
||||
IsTransparent = isTransparent,
|
||||
IsAdditive = isAdditive,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -174,6 +174,13 @@ public class TextureBatchData {
|
|||
public bool IsAdditive { get; set; }
|
||||
public bool HasWrappingUVs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retail material alpha derived from the resolved source Surface. This is
|
||||
/// <c>1 - Surface.Translucency</c> when the Translucent bit is set, and 1
|
||||
/// otherwise. It is distinct from decoded base-texture alpha.
|
||||
/// </summary>
|
||||
public float SurfaceOpacity { get; set; } = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Retail source surface-array index this subset was constructed from
|
||||
/// (contract §3.6) — the CellStruct subset/material OWNER, not the
|
||||
|
|
|
|||
|
|
@ -228,6 +228,9 @@ 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
|
||||
// identity rejects older payloads before this serializer is entered.
|
||||
w.Write(batch.SurfaceOpacity);
|
||||
}
|
||||
|
||||
private static TextureBatchData ReadTextureBatchData(
|
||||
|
|
@ -252,6 +255,7 @@ public static class ObjectMeshDataSerializer {
|
|||
batch.RetailSurfaceMask = r.ReadByte();
|
||||
batch.RawSurfaceType = r.ReadUInt32();
|
||||
batch.IsCellShell = r.ReadBoolean();
|
||||
batch.SurfaceOpacity = r.ReadSingle();
|
||||
return batch;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,10 +43,11 @@ public static class PakFormat {
|
|||
/// source surface-array index rather than TextureKey/stippling/sides,
|
||||
/// and built-EnvCell admission is
|
||||
/// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after
|
||||
/// surface resolution. The binary format remains version 2, but every
|
||||
/// prepared CellStruct/EnvCell render record must be regenerated.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public const uint CurrentBakeToolVersion = 8;
|
||||
public const uint CurrentBakeToolVersion = 9;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ public static class ContentMigrationCatalog
|
|||
7,
|
||||
8,
|
||||
"exact CellStruct surface-index subset construction"),
|
||||
[9] = FullRebuild(
|
||||
8,
|
||||
9,
|
||||
"authored surface opacity in prepared mesh batches"),
|
||||
};
|
||||
|
||||
public static ContentMigrationPlan Resolve(uint fromRecipeVersion, uint targetRecipeVersion)
|
||||
|
|
|
|||
|
|
@ -38,9 +38,10 @@ public sealed class LauncherInstallRecordStore
|
|||
// Version 7 keeps pak format 2 and regenerates every GfxObj render record
|
||||
// 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. Both are
|
||||
// mandatory full rebuilds from their predecessor recipe.
|
||||
public const uint CurrentBakeToolVersion = 8;
|
||||
// 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;
|
||||
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -90,11 +90,12 @@ public static class RenderPackSpirvValidator
|
|||
new([0u, 1u], [], true, true, true, false, false),
|
||||
RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver =>
|
||||
new([], [1u, 2u, 3u], true, false, true, false, true),
|
||||
// Campaign VM VM6: mesh_atmospheric.vert now reads
|
||||
// uAtmosphereClockWind/uAtmosphereWindAmplitude for the same
|
||||
// foliage-wind displacement the casters apply.
|
||||
// Campaign VM VM6: mesh_atmospheric.vert reads the atmospheric
|
||||
// clock/wind inputs for the same foliage displacement the casters
|
||||
// apply. Campaign OVERHAUL S5-c4 adds binding 9 for the per-instance
|
||||
// detail category used by the retail one-pass material combine.
|
||||
RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver =>
|
||||
new([0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u], [1u],
|
||||
new([0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u], [1u],
|
||||
true, true, true, false, true),
|
||||
_ => new(
|
||||
[],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue