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
File diff suppressed because one or more lines are too long
|
|
@ -143,17 +143,6 @@ internal enum GpuBlendMode
|
||||||
/// `ParticleRenderer` needs it too (slice V4e).
|
/// `ParticleRenderer` needs it too (slice V4e).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
InverseAlpha,
|
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
|
internal enum GpuCompareOp
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ internal struct GpuPushConstants
|
||||||
public uint TextureIndexA;
|
public uint TextureIndexA;
|
||||||
|
|
||||||
/// <summary>GLSL <c>uTextureIndexB</c>. Secondary per-pass slot; terrain
|
/// <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>
|
/// carry the absolute transform-prefix instance count.</summary>
|
||||||
public uint TextureIndexB;
|
public uint TextureIndexB;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ internal sealed record VulkanDeviceFeatureSupport
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// #459: the shader compiler targets Vulkan 1.3 / SPIR-V 1.6, where glslang
|
/// #459: the shader compiler targets Vulkan 1.3 / SPIR-V 1.6, where glslang
|
||||||
/// lowers every fragment <c>discard</c> to <c>OpDemoteToHelperInvocation</c>
|
/// 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
|
/// mesh_atmospheric and both directional-shadow cutout fragments). A module
|
||||||
/// that declares the capability without the feature enabled is invalid
|
/// that declares the capability without the feature enabled is invalid
|
||||||
/// usage (<c>VUID-VkShaderModuleCreateInfo-pCode-08740</c>), so the feature
|
/// usage (<c>VUID-VkShaderModuleCreateInfo-pCode-08740</c>), so the feature
|
||||||
|
|
|
||||||
|
|
@ -178,12 +178,6 @@ internal static class VulkanViewportMapping
|
||||||
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
GpuBlendMode.Additive => (BlendFactor.SrcAlpha, BlendFactor.One),
|
||||||
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
// Retail's third mode, found at slice V4c in WbDrawDispatcher.ApplyRetailBlend.
|
||||||
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
GpuBlendMode.InverseAlpha => (BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||||
// 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),
|
GpuBlendMode.None => (BlendFactor.One, BlendFactor.Zero),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(blend), blend, "Unknown blend mode."),
|
_ => throw new ArgumentOutOfRangeException(nameof(blend), blend, "Unknown blend mode."),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering.Gpu;
|
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
namespace AcDream.App.Rendering;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Testable CPU statement of retail's detail-pass gate and pixel math. The
|
/// Testable CPU statement of retail's detail gate and one-pass pixel math. The
|
||||||
/// production pixels are produced by <c>mesh_detail</c>; keeping these facts
|
/// production pixels are produced by both world-mesh fragment families; keeping these facts
|
||||||
/// in one small contract makes the setting and the combine independently
|
/// in one small contract makes the setting and the combine independently
|
||||||
/// assertable without a GPU.
|
/// assertable without a GPU.
|
||||||
///
|
///
|
||||||
|
|
@ -17,15 +16,12 @@ namespace AcDream.App.Rendering;
|
||||||
/// <c>D3DPolyRender::RenderMeshSubset</c> (0x0059ca10) never falls back to
|
/// <c>D3DPolyRender::RenderMeshSubset</c> (0x0059ca10) never falls back to
|
||||||
/// the two-pass framebuffer blend the earlier #226 port reproduced; it takes
|
/// the two-pass framebuffer blend the earlier #226 port reproduced; it takes
|
||||||
/// the single-pass texture-stage combine set up in
|
/// the single-pass texture-stage combine set up in
|
||||||
/// <c>D3DPolyRender::SetSurface</c> (0x0059c4d0):
|
/// <c>D3DPolyRender::SetSurface</c> (0x0059c4d0). With base RGB <c>B</c>,
|
||||||
/// <c>lerp(base * diffuse, detail.rgb, detail.a * diffuse.a)</c> — a blend
|
/// diffuse RGB <c>D</c>, active material alpha <c>a</c>, and detail texel
|
||||||
/// TOWARD the detail colour by <c>detail.a * diffuse.a</c>, not the
|
/// <c>Q=(q,qA)</c>, that stage program emits
|
||||||
/// fallback's <c>dest * (detail.rgb + 1 - detail.a)</c>. Built meshes light
|
/// <c>C=q*(a*qA)+(B*D)*(1-a*qA)</c> and <c>X=a*qA*qA</c>. Base texture alpha
|
||||||
/// with <c>tmpmaterial.Diffuse.a = 1</c> for opaque subsets
|
/// is deliberately excluded. This is not the fallback's framebuffer replay
|
||||||
/// (<c>RenderMeshSubset</c>), so on the live Dereth category texture (mean
|
/// result <c>dest * (detail.rgb + 1 - detail.a)</c>.</para>
|
||||||
/// 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>
|
|
||||||
///
|
///
|
||||||
/// <para>There is no distance fade on this path. Retail's
|
/// <para>There is no distance fade on this path. Retail's
|
||||||
/// <c>ACRender::get_alpha_for_z</c> (0x006b6230) is only evaluated in
|
/// <c>ACRender::get_alpha_for_z</c> (0x006b6230) is only evaluated in
|
||||||
|
|
@ -44,66 +40,69 @@ internal static class RetailDetailTextureContract
|
||||||
settingEnabled && binding.IsAvailable;
|
settingEnabled && binding.IsAvailable;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Opaque detail must compare equal against the depth written by its exact
|
/// Retail's exact pre-fog stage result. Base alpha is deliberately absent.
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static GpuCompareOp DetailDepthCompare(bool transparent) =>
|
internal static Vector4 Combine(
|
||||||
transparent ? GpuCompareOp.LessOrEqual : GpuCompareOp.Equal;
|
Vector4 baseTexel,
|
||||||
|
Vector3 diffuse,
|
||||||
/// <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,
|
|
||||||
Vector4 detail,
|
Vector4 detail,
|
||||||
float opacity,
|
float authoredOpacity,
|
||||||
Vector3 fog,
|
float liveOpacity)
|
||||||
float fogFactor)
|
|
||||||
{
|
{
|
||||||
Vector3 foggedBase = Vector3.Lerp(baseColour, fog, fogFactor);
|
float alpha = authoredOpacity * liveOpacity;
|
||||||
Vector3 foggedDetail = Vector3.Lerp(
|
float weight = alpha * detail.W;
|
||||||
new Vector3(detail.X, detail.Y, detail.Z),
|
Vector3 colour = new(detail.X, detail.Y, detail.Z);
|
||||||
fog,
|
colour *= weight;
|
||||||
fogFactor);
|
colour += new Vector3(baseTexel.X, baseTexel.Y, baseTexel.Z)
|
||||||
return Vector3.Lerp(foggedBase, foggedDetail, detail.W * opacity);
|
* 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 float vOpacityMultiplier; // #188
|
||||||
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
||||||
in flat uint vReceivesDirectionalShadow;
|
in flat uint vReceivesDirectionalShadow;
|
||||||
|
in flat float vSurfaceOpacity;
|
||||||
|
in flat uint vBatchFlags;
|
||||||
|
in flat uint vDetailCategory;
|
||||||
|
|
||||||
#include "directional_shadow_receiver.glsl"
|
#include "directional_shadow_receiver.glsl"
|
||||||
|
#include "retail_detail_material.glsl"
|
||||||
|
|
||||||
// uRenderPass values (Phase N.5 Decision 2 — two-pass alpha-test):
|
// uRenderPass values (Phase N.5 Decision 2 — two-pass alpha-test):
|
||||||
// 0 = opaque pass — discard fragments with alpha < 0.95
|
// 0 = opaque pass — discard fragments with alpha < 0.95
|
||||||
|
|
@ -26,6 +30,9 @@ in flat uint vReceivesDirectionalShadow;
|
||||||
// transparent overdraw cost otherwise)
|
// transparent overdraw cost otherwise)
|
||||||
uniform int uRenderPass;
|
uniform int uRenderPass;
|
||||||
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
|
||||||
|
uniform uint uTextureIndexA;
|
||||||
|
uniform float uParamA;
|
||||||
|
uniform float uParamB;
|
||||||
|
|
||||||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||||||
struct Light {
|
struct Light {
|
||||||
|
|
@ -61,35 +68,14 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
||||||
|
|
||||||
out vec4 FragColor;
|
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() {
|
void main() {
|
||||||
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
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)
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the authored outdoor directional term is shadowed. Ambient, local
|
// Only the authored outdoor directional term is shadowed. Ambient, local
|
||||||
// lights and material selection pulses remain exactly on the retail path.
|
// 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 =
|
// ignored). Stripes visible HERE = a vertex-lighting artifact; absent =
|
||||||
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
|
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
|
||||||
if (uLightDebug == 3) {
|
if (uLightDebug == 3) {
|
||||||
|
if (color.a < alphaCutoff) discard;
|
||||||
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
|
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -120,11 +107,30 @@ void main() {
|
||||||
// Retail clamp per-channel to 1.0 (r13 §13.1).
|
// Retail clamp per-channel to 1.0 (r13 §13.1).
|
||||||
lit = min(lit, vec3(1.0));
|
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);
|
rgb = applyFog(rgb, vWorldPos);
|
||||||
// #188: multiply the FINAL alpha only — the discard thresholds above stay
|
FragColor = vec4(rgb, alpha);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ struct InstanceData {
|
||||||
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
||||||
struct BatchData {
|
struct BatchData {
|
||||||
uint textureIndex; // slot into the global texture table
|
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 textureLayer; // layer in the shared WB or pooled composite array
|
||||||
uint flags; // reserved — N.5 dispatcher owns all blend state
|
uint flags; // reserved — N.5 dispatcher owns all blend state
|
||||||
// (glBlendFunc per pass). If a future phase wants
|
// (glBlendFunc per pass). If a future phase wants
|
||||||
|
|
@ -116,6 +116,10 @@ layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
|
||||||
vec2 instanceSelectionLighting[];
|
vec2 instanceSelectionLighting[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
||||||
|
uint instanceDetailCategory[];
|
||||||
|
};
|
||||||
|
|
||||||
uniform mat4 uViewProjection;
|
uniform mat4 uViewProjection;
|
||||||
// Absolute transform prefix in the shared shadow/world pose arena. Every
|
// Absolute transform prefix in the shared shadow/world pose arena. Every
|
||||||
// parallel per-instance array remains local to this submission, so only the
|
// 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 float vOpacityMultiplier; // #188
|
||||||
out flat vec2 vSelectionLighting;
|
out flat vec2 vSelectionLighting;
|
||||||
out flat uint vReceivesDirectionalShadow;
|
out flat uint vReceivesDirectionalShadow;
|
||||||
|
out flat float vSurfaceOpacity;
|
||||||
|
out flat uint vBatchFlags;
|
||||||
|
out flat uint vDetailCategory;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
|
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
|
||||||
|
|
@ -358,4 +365,7 @@ void main() {
|
||||||
// could read its flags — Campaign VM VM6.)
|
// could read its flags — Campaign VM VM6.)
|
||||||
vTextureIndex = b.textureIndex;
|
vTextureIndex = b.textureIndex;
|
||||||
vTextureLayer = b.textureLayer;
|
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 uint vTextureLayer;
|
||||||
in flat float vOpacityMultiplier; // #188
|
in flat float vOpacityMultiplier; // #188
|
||||||
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
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):
|
// uRenderPass values (Phase N.5 Decision 2 — two-pass alpha-test):
|
||||||
// 0 = opaque pass — discard fragments with alpha < 0.95
|
// 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
|
// recognized, supplied per MDI range from TextureKey.PaletteId (100/255
|
||||||
// paletted, 200/255 DDS/non-paletted). Every other value keeps 0.05.
|
// paletted, 200/255 DDS/non-paletted). Every other value keeps 0.05.
|
||||||
uniform float uParamB;
|
uniform float uParamB;
|
||||||
|
uniform uint uTextureIndexA;
|
||||||
|
uniform float uParamA;
|
||||||
|
|
||||||
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
|
||||||
struct Light {
|
struct Light {
|
||||||
|
|
@ -70,35 +77,6 @@ void main() {
|
||||||
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
||||||
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
|
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
|
// Per-vertex Gouraud lighting from the vertex shader (ambient + capped
|
||||||
// lights), passed through retail CMaterial's two click-pulse replacements:
|
// lights), passed through retail CMaterial's two click-pulse replacements:
|
||||||
// emissive/luminosity + diffuse * scene lighting. Normal is (0,1).
|
// emissive/luminosity + diffuse * scene lighting. Normal is (0,1).
|
||||||
|
|
@ -108,6 +86,7 @@ void main() {
|
||||||
// ignored). Stripes visible HERE = a vertex-lighting artifact; absent =
|
// ignored). Stripes visible HERE = a vertex-lighting artifact; absent =
|
||||||
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
|
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
|
||||||
if (uLightDebug == 3) {
|
if (uLightDebug == 3) {
|
||||||
|
if (color.a < alphaCutoff) discard;
|
||||||
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
|
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -118,11 +97,32 @@ void main() {
|
||||||
// Retail clamp per-channel to 1.0 (r13 §13.1).
|
// Retail clamp per-channel to 1.0 (r13 §13.1).
|
||||||
lit = min(lit, vec3(1.0));
|
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);
|
rgb = applyFog(rgb, vWorldPos);
|
||||||
// #188: multiply the FINAL alpha only — the discard thresholds above stay
|
FragColor = vec4(rgb, alpha);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ struct InstanceData {
|
||||||
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
|
||||||
struct BatchData {
|
struct BatchData {
|
||||||
uint textureIndex; // slot into the global texture table
|
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 textureLayer; // layer in the shared WB or pooled composite array
|
||||||
uint flags; // reserved — N.5 dispatcher owns all blend state
|
uint flags; // reserved — N.5 dispatcher owns all blend state
|
||||||
// (glBlendFunc per pass). If a future phase wants
|
// (glBlendFunc per pass). If a future phase wants
|
||||||
|
|
@ -112,6 +112,10 @@ layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
|
||||||
vec2 instanceSelectionLighting[];
|
vec2 instanceSelectionLighting[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
|
||||||
|
uint instanceDetailCategory[];
|
||||||
|
};
|
||||||
|
|
||||||
uniform mat4 uViewProjection;
|
uniform mat4 uViewProjection;
|
||||||
|
|
||||||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
|
// 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 uint vTextureLayer;
|
||||||
out flat float vOpacityMultiplier; // #188
|
out flat float vOpacityMultiplier; // #188
|
||||||
out flat vec2 vSelectionLighting;
|
out flat vec2 vSelectionLighting;
|
||||||
|
out flat float vSurfaceOpacity;
|
||||||
|
out flat uint vBatchFlags;
|
||||||
|
out flat uint vDetailCategory;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
int instanceIndex = gl_BaseInstanceARB + gl_InstanceID;
|
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.
|
// site in mesh_modern.frag, which is the only form Vulkan can express.
|
||||||
vTextureIndex = b.textureIndex;
|
vTextureIndex = b.textureIndex;
|
||||||
vTextureLayer = b.textureLayer;
|
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": [
|
"stages": [
|
||||||
{
|
{
|
||||||
"stage": "vert",
|
"stage": "vert",
|
||||||
"sourceSha256": "e552edb7ef42e9e32812824c075310f2d99eb5826e527db5c22b54890ef02088",
|
"sourceSha256": "71950823770a69d610db03e0c54c645934e3b46a686c8abac85b52f2476afcfa",
|
||||||
"compiled": true
|
"compiled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "62b001a72080ea74bdaef0fb2a4ed92533feaeab76187ce4215813c6327432e7",
|
"sourceSha256": "5eb058e92ab27f1f6c245401e05f78a278fe00696b5c77f36ae33f8428f83bf3",
|
||||||
"compiled": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mesh_detail",
|
|
||||||
"vulkanReady": true,
|
|
||||||
"stages": [
|
|
||||||
{
|
|
||||||
"stage": "vert",
|
|
||||||
"sourceSha256": "2d8b5b42f053a2d42e1ff44db3e331d5bea2d3f7dd36e20d813b7ae22c033181",
|
|
||||||
"compiled": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"stage": "frag",
|
|
||||||
"sourceSha256": "a5285c51e8d6a1fa91f403f8489819595fa5a20abaaaccc969f220ea2c4eb335",
|
|
||||||
"compiled": true
|
"compiled": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -247,12 +231,12 @@
|
||||||
"stages": [
|
"stages": [
|
||||||
{
|
{
|
||||||
"stage": "vert",
|
"stage": "vert",
|
||||||
"sourceSha256": "dba51ce23f2b621683fc78db73f608a0c000479b8e827cd3ecc8731d46c6ee64",
|
"sourceSha256": "c41448edcd258313f921f3b66c0884c8f78d4d592eef1abe0804c6cb2890c58f",
|
||||||
"compiled": true
|
"compiled": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "d6ae09de279e814b159e7c481d6b66f3cc37c741b3d8a249fbe2bc8156e52d4e",
|
"sourceSha256": "396c382af73505bc2cac7a2db5878a63166730e47096a94630c35ad8376fe565",
|
||||||
"compiled": true
|
"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-
|
/// 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
|
/// write, no-blend pipeline pair the walk's ordered stream actually uses
|
||||||
/// TODAY — not <c>StreamMark</c> itself: if any future ordered-stream
|
/// TODAY — not <c>StreamMark</c> itself: if any future ordered-stream
|
||||||
/// stage draws with blending or a disabled depth write (for example the
|
/// stage draws with blending or a disabled depth write, that stage's
|
||||||
/// deferred building-detail overlay replay,
|
/// <c>StreamMark</c> MUST
|
||||||
/// <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
|
|
||||||
/// become a flush point again; the contract gets amended, not bent.</para>
|
/// become a flush point again; the contract gets amended, not bent.</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void Replay(IGpuFrame frame, IGpuPassEncoder encoder)
|
internal void Replay(IGpuFrame frame, IGpuPassEncoder encoder)
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,6 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
private IGpuPipeline? _alphaPipeline;
|
private IGpuPipeline? _alphaPipeline;
|
||||||
private IGpuPipeline? _clipPipeline;
|
private IGpuPipeline? _clipPipeline;
|
||||||
private IGpuPipeline? _additivePipeline;
|
private IGpuPipeline? _additivePipeline;
|
||||||
private IGpuPipeline? _detailPipeline;
|
|
||||||
private IGpuPipeline? _transparentDetailPipeline;
|
|
||||||
private IGpuPipeline? _clipDetailPipeline;
|
|
||||||
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail;
|
||||||
private readonly Func<bool> _buildingDetailEnabled;
|
private readonly Func<bool> _buildingDetailEnabled;
|
||||||
|
|
||||||
|
|
@ -46,11 +43,12 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
/// to the retail alpha queue for later replay.
|
/// to the retail alpha queue for later replay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal bool TransparentDetailEnabled =>
|
internal bool TransparentDetailEnabled =>
|
||||||
RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _environmentDetail);
|
RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _environmentDetail)
|
||||||
|
&& _environmentDetail.Tiling != 0f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
/// The RHI arm's constructor. It also completes <c>Initialize</c>'s job: the
|
||||||
/// 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.
|
/// and no <c>Shader</c> to hand in.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal EnvCellRenderer(
|
internal EnvCellRenderer(
|
||||||
|
|
@ -82,33 +80,6 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
scope.SampleCount);
|
scope.SampleCount);
|
||||||
_additivePipeline = CreateShellPipeline(
|
_additivePipeline = CreateShellPipeline(
|
||||||
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
device, "envcell-additive", GpuBlendMode.Additive, depthWrite: false, scope.SampleCount);
|
||||||
_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;
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,7 +238,7 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
BindRingSection<int>(
|
BindRingSection<int>(
|
||||||
encoder, frame, GpuBindingModel.StorageInstanceLightSets,
|
encoder, frame, GpuBindingModel.StorageInstanceLightSets,
|
||||||
_lightSetData.AsSpan(0, uniqueInstanceCount * lightStride));
|
_lightSetData.AsSpan(0, uniqueInstanceCount * lightStride));
|
||||||
BindEnvironmentDetailCategory(encoder, frame);
|
BindEnvironmentDetailCategory(encoder, frame, uniqueInstanceCount);
|
||||||
|
|
||||||
// The frame-global sections, bound after this renderer's own binds
|
// The frame-global sections, bound after this renderer's own binds
|
||||||
// because those binds are what select the descriptor scope.
|
// 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);
|
MemoryMarshal.AsBytes(_commands.AsSpan(0, totalDraws)).CopyTo(commands.Data);
|
||||||
IGpuBuffer commandBuffer = commands.Buffer;
|
IGpuBuffer commandBuffer = commands.Buffer;
|
||||||
uint commandBase = commands.OffsetBytes;
|
uint commandBase = commands.OffsetBytes;
|
||||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
bool detailEnabled = TransparentDetailEnabled;
|
||||||
_buildingDetailEnabled(),
|
|
||||||
_environmentDetail);
|
|
||||||
|
|
||||||
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
for (int drawRangeIndex = 0; drawRangeIndex < _mdiDrawRanges.Count; drawRangeIndex++)
|
||||||
{
|
{
|
||||||
|
|
@ -324,56 +293,14 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
pushConstants.ParamB = isClip
|
pushConstants.ParamB = isClip
|
||||||
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
? groupIndex >= ClipPalettedGroupBase ? 100f / 255f : 200f / 255f
|
||||||
: 0f;
|
: 0f;
|
||||||
|
pushConstants.TextureIndexA = detailEnabled
|
||||||
|
? _environmentDetail.TextureSlot.Index
|
||||||
|
: 0u;
|
||||||
|
pushConstants.ParamA = detailEnabled
|
||||||
|
? _environmentDetail.Tiling
|
||||||
|
: 0f;
|
||||||
encoder.SetPushConstants(in pushConstants);
|
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(
|
encoder.MultiDrawIndexedIndirect(
|
||||||
commandBuffer,
|
commandBuffer,
|
||||||
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
|
commandBase + (uint)(drawRange.FirstCommand * sizeof(DrawElementsIndirectCommand)),
|
||||||
|
|
@ -381,39 +308,10 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
(uint)sizeof(DrawElementsIndirectCommand));
|
(uint)sizeof(DrawElementsIndirectCommand));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retail DrawEnvCell category (2). Replay only the already-filtered
|
pushConstants.TextureIndexA = 0;
|
||||||
// opaque shell commands. Transparent ClipMap is excluded here: it
|
pushConstants.ParamA = 0f;
|
||||||
// takes the immediate/delayed CLIP path and, when detail is active,
|
pushConstants.ParamB = 0f;
|
||||||
// its own _clipDetailPipeline contribution. No distance fade
|
encoder.SetPushConstants(in pushConstants);
|
||||||
// (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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BindPipelineWithMesh(
|
private void BindPipelineWithMesh(
|
||||||
|
|
@ -502,23 +400,23 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Binds one category word for <c>mesh_detail.vert</c>'s statically used
|
/// Binds one EnvCell category word per instance for the world vertex
|
||||||
/// binding 9. The vertex shader treats only exact <c>uParamB=1</c> as the
|
/// shader's statically used binding 9. Vulkan requires the declared
|
||||||
/// object/building-category sentinel; ordinary EnvCell zero and the CLIP
|
/// descriptor to be valid even when detail is unarmed.
|
||||||
/// references 100/255 and 200/255 therefore keep the environment category.
|
|
||||||
/// Vulkan still requires the declared descriptor to be valid.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static void BindEnvironmentDetailCategory(
|
internal void BindEnvironmentDetailCategory(
|
||||||
IGpuPassEncoder encoder,
|
IGpuPassEncoder encoder,
|
||||||
IGpuFrame frame)
|
IGpuFrame frame,
|
||||||
|
int instanceCount)
|
||||||
{
|
{
|
||||||
Span<uint> category = stackalloc uint[1];
|
if (_detailCategoryData.Length < instanceCount)
|
||||||
category[0] = 1u;
|
Array.Resize(ref _detailCategoryData, Math.Max(instanceCount, 16));
|
||||||
|
Array.Fill(_detailCategoryData, 1u, 0, instanceCount);
|
||||||
BindRingSection<uint>(
|
BindRingSection<uint>(
|
||||||
encoder,
|
encoder,
|
||||||
frame,
|
frame,
|
||||||
GpuBindingModel.StorageInstanceDetailCategory,
|
GpuBindingModel.StorageInstanceDetailCategory,
|
||||||
category);
|
_detailCategoryData.AsSpan(0, instanceCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeRhiResources()
|
private void DisposeRhiResources()
|
||||||
|
|
@ -531,11 +429,5 @@ public sealed unsafe partial class EnvCellRenderer
|
||||||
_clipPipeline = null;
|
_clipPipeline = null;
|
||||||
_additivePipeline?.Dispose();
|
_additivePipeline?.Dispose();
|
||||||
_additivePipeline = null;
|
_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>();
|
private uint[] _clipSlotData = Array.Empty<uint>();
|
||||||
|
|
||||||
// Campaign VM VM1 follow-up: per-instance opacity multiplier, parallel to
|
// 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).
|
// InstanceAlphaBuf (binding 7, GpuBindingModel.StorageInstanceAlpha).
|
||||||
// Before this field the shell and interior-detail passes never bound that
|
// Before this field the shell pass never bound that storage buffer at
|
||||||
// storage buffer at all, so both shaders read whatever section
|
// all, so the shader read whatever section
|
||||||
// WbDrawDispatcher's own SubmitRhi last bound in the same pass — an
|
// WbDrawDispatcher's own SubmitRhi last bound in the same pass — an
|
||||||
// unrelated object's opacity array, indexed by these cell instance ids.
|
// unrelated object's opacity array, indexed by these cell instance ids.
|
||||||
// EnvCell shells never carry a #188 TransparentPartHook translucency fade
|
// 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<...>()
|
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||||
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
|
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
|
||||||
private ModernBatchData[] _modernBatches = Array.Empty<ModernBatchData>();
|
private ModernBatchData[] _modernBatches = Array.Empty<ModernBatchData>();
|
||||||
|
private uint[] _detailCategoryData = Array.Empty<uint>();
|
||||||
private readonly List<EnvCellLandblock> _prepareLandblocks = new();
|
private readonly List<EnvCellLandblock> _prepareLandblocks = new();
|
||||||
private readonly List<InstanceData> _renderInstances = new();
|
private readonly List<InstanceData> _renderInstances = new();
|
||||||
private readonly List<(ObjectRenderData renderData, ulong gfxObjId, int count, int offset)> _renderDrawCalls = 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,
|
EnvCellTransparentRoute transparentRoute,
|
||||||
bool detailSurfaceActive)
|
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
|
// at construction (see EnvCellRenderer.Rhi.cs), so _initialized alone
|
||||||
// answers whether this renderer is ready to draw.
|
// answers whether this renderer is ready to draw.
|
||||||
if (!_initialized) return;
|
if (!_initialized) return;
|
||||||
|
|
@ -1312,6 +1313,7 @@ public sealed partial class EnvCellRenderer :
|
||||||
// Slice V4t: the slot is the device's, assigned by the
|
// Slice V4t: the slot is the device's, assigned by the
|
||||||
// mesh manager at upload rather than interned here.
|
// mesh manager at upload rather than interned here.
|
||||||
TextureTableIndex = item.batch.TextureSlot.Index,
|
TextureTableIndex = item.batch.TextureSlot.Index,
|
||||||
|
SurfaceOpacity = item.batch.SurfaceOpacity,
|
||||||
TextureIndex = (uint)item.batch.TextureIndex,
|
TextureIndex = (uint)item.batch.TextureIndex,
|
||||||
// #226: this renderer submits built EnvCell meshes.
|
// #226: this renderer submits built EnvCell meshes.
|
||||||
// Retail DrawMesh forwards curr_detail_surface to
|
// Retail DrawMesh forwards curr_detail_surface to
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ namespace AcDream.App.Rendering.Wb;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Campaign VM VM6: the two <c>BatchData.flags</c> bits that mark a world
|
/// 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
|
/// 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
|
/// never touched here. <c>mesh_atmospheric.vert</c> and the four
|
||||||
/// <c>directional_shadow_world_*.vert</c> caster shaders read these bits
|
/// <c>directional_shadow_world_*.vert</c> caster shaders read these bits
|
||||||
/// through the shared <c>foliage_wind.glsl</c> include
|
/// through the shared <c>foliage_wind.glsl</c> include
|
||||||
|
|
|
||||||
|
|
@ -52,4 +52,5 @@ internal readonly record struct GroupKey(
|
||||||
uint TextureLayer,
|
uint TextureLayer,
|
||||||
TranslucencyKind Translucency,
|
TranslucencyKind Translucency,
|
||||||
uint FoliageFlags,
|
uint FoliageFlags,
|
||||||
|
float SurfaceOpacity = 1f,
|
||||||
CullMode CullMode = CullMode.CounterClockwise);
|
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
|
/// 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.
|
/// bindless handle) and the layer index within the shared/pooled array.
|
||||||
/// Indexed by gl_DrawIDARB in the vertex shader. Same 16-byte std430 shape
|
/// 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.
|
/// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||||
public struct ModernBatchData {
|
public struct ModernBatchData {
|
||||||
public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table
|
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 TextureIndex; // 4 bytes — layer within the texture array
|
||||||
public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags
|
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 IsTransparent { get; set; }
|
||||||
public bool IsAdditive { get; set; }
|
public bool IsAdditive { get; set; }
|
||||||
public bool HasWrappingUVs { 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
|
// Modern rendering path fields
|
||||||
public uint FirstIndex { get; set; }
|
public uint FirstIndex { get; set; }
|
||||||
|
|
@ -2263,6 +2265,7 @@ namespace AcDream.App.Rendering.Wb
|
||||||
IsTransparent = batch.IsTransparent,
|
IsTransparent = batch.IsTransparent,
|
||||||
IsAdditive = batch.IsAdditive,
|
IsAdditive = batch.IsAdditive,
|
||||||
HasWrappingUVs = batch.HasWrappingUVs,
|
HasWrappingUVs = batch.HasWrappingUVs,
|
||||||
|
SurfaceOpacity = batch.SurfaceOpacity,
|
||||||
Key = batch.Key,
|
Key = batch.Key,
|
||||||
CullMode = batch.CullMode,
|
CullMode = batch.CullMode,
|
||||||
FirstIndex = firstIndex,
|
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
|
/// calls the pair (prepare once, draw the whole stream as one range, or as
|
||||||
/// several — see <c>OrderPreservingSubmitterTests</c>).</para>
|
/// several — see <c>OrderPreservingSubmitterTests</c>).</para>
|
||||||
///
|
///
|
||||||
/// <para>Scope: this stage proves walk-order submission through the existing
|
/// <para>OVERHAUL S5-c4 arms retail detail on a detail-category command's
|
||||||
/// RHI on static content (plan §FW2). It does NOT wire the retail
|
/// existing solo base run. It remains one draw and therefore introduces no
|
||||||
/// building-detail overlay replay (<c>DrawBuildingDetailRangeRhi</c>'s second
|
/// blended overlay or additional terrain/StreamMark flush boundary.</para>
|
||||||
/// 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>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed unsafe partial class WbDrawDispatcher
|
public sealed unsafe partial class WbDrawDispatcher
|
||||||
{
|
{
|
||||||
|
|
@ -395,6 +390,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
_batchData[i] = new BatchData
|
_batchData[i] = new BatchData
|
||||||
{
|
{
|
||||||
TextureIndex = key.TextureSlot.Index,
|
TextureIndex = key.TextureSlot.Index,
|
||||||
|
SurfaceOpacity = key.SurfaceOpacity,
|
||||||
TextureLayer = key.TextureLayer,
|
TextureLayer = key.TextureLayer,
|
||||||
Flags = 1u | key.FoliageFlags,
|
Flags = 1u | key.FoliageFlags,
|
||||||
};
|
};
|
||||||
|
|
@ -545,6 +541,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
IGpuBuffer commandBuffer = _orderedCommands.Buffer!;
|
IGpuBuffer commandBuffer = _orderedCommands.Buffer!;
|
||||||
uint commandBase = _orderedCommands.OffsetBytes;
|
uint commandBase = _orderedCommands.OffsetBytes;
|
||||||
int rangeEnd = firstCommand + commandCount;
|
int rangeEnd = firstCommand + commandCount;
|
||||||
|
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||||
|
_buildingDetailEnabled(),
|
||||||
|
_buildingDetail)
|
||||||
|
&& _buildingDetail.Tiling != 0f;
|
||||||
|
|
||||||
foreach (OrderedMergeRun run in _orderedRuns)
|
foreach (OrderedMergeRun run in _orderedRuns)
|
||||||
{
|
{
|
||||||
|
|
@ -569,6 +569,11 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
PipelineBucket bucket = BucketFor(_orderedStream.Keys[run.FirstCommand].Translucency);
|
PipelineBucket bucket = BucketFor(_orderedStream.Keys[run.FirstCommand].Translucency);
|
||||||
IGpuPipeline pipeline = PipelineForBucket(pipelines, bucket);
|
IGpuPipeline pipeline = PipelineForBucket(pipelines, bucket);
|
||||||
pushConstants.RenderPass = bucket == PipelineBucket.Opaque ? 0 : 1;
|
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);
|
BindPipelineWithMesh(encoder, pipeline, global);
|
||||||
DrawIndirectRangeRhi(
|
DrawIndirectRangeRhi(
|
||||||
|
|
@ -580,6 +585,9 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
run.FirstCommand,
|
run.FirstCommand,
|
||||||
run.CommandCount);
|
run.CommandCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClearDetailPushConstants(ref pushConstants);
|
||||||
|
encoder.SetPushConstants(in pushConstants);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
private readonly IWorldPassScope? _scope;
|
private readonly IWorldPassScope? _scope;
|
||||||
|
|
||||||
/// <summary>
|
/// <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
|
/// <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
|
/// 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 OpaqueAlphaToCoverage,
|
||||||
IGpuPipeline AlphaBlend,
|
IGpuPipeline AlphaBlend,
|
||||||
IGpuPipeline AlphaAdditive,
|
IGpuPipeline AlphaAdditive,
|
||||||
IGpuPipeline AlphaInverse,
|
IGpuPipeline AlphaInverse);
|
||||||
IGpuPipeline RetailDetail,
|
|
||||||
IGpuPipeline RetailDetailTransparent);
|
|
||||||
|
|
||||||
private MeshPipelineSet? _backbufferPipelines;
|
private MeshPipelineSet? _backbufferPipelines;
|
||||||
private MeshPipelineSet? _offscreenPipelines;
|
private MeshPipelineSet? _offscreenPipelines;
|
||||||
|
|
||||||
private const string OpaqueTimerScope = "wb-entities-opaque";
|
private const string OpaqueTimerScope = "wb-entities-opaque";
|
||||||
private const string TransparentTimerScope = "wb-entities-transparent";
|
private const string TransparentTimerScope = "wb-entities-transparent";
|
||||||
private const string DetailTimerScope = "wb-buildings-detail";
|
|
||||||
|
|
||||||
private readonly TerrainAtlas.RetailDetailTextureBinding _buildingDetail;
|
private readonly TerrainAtlas.RetailDetailTextureBinding _buildingDetail;
|
||||||
private readonly Func<bool> _buildingDetailEnabled;
|
private readonly Func<bool> _buildingDetailEnabled;
|
||||||
|
|
@ -158,8 +155,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The RHI arm's constructor. No GL context, no <c>Shader</c>, no
|
/// 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
|
/// <c>BindlessSupport</c>: five base pipelines compile <c>mesh_modern</c>;
|
||||||
/// the detail overlay compiles <c>mesh_detail</c> from committed SPIR-V;
|
|
||||||
/// batch data already carries the device's own
|
/// batch data already carries the device's own
|
||||||
/// <c>GpuTextureSlot</c> (V4t) rather than a bindless handle.
|
/// <c>GpuTextureSlot</c> (V4t) rather than a bindless handle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -231,7 +227,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
bool usesRenderPackShaderAbi = false)
|
bool usesRenderPackShaderAbi = false)
|
||||||
{
|
{
|
||||||
string suffix = samples > 1 ? string.Empty : "-1x";
|
string suffix = samples > 1 ? string.Empty : "-1x";
|
||||||
var created = new List<IGpuPipeline>(7);
|
var created = new List<IGpuPipeline>(5);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new MeshPipelineSet(
|
return new MeshPipelineSet(
|
||||||
|
|
@ -260,28 +256,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples,
|
device, $"{namePrefix}-inverse{suffix}", GpuBlendMode.InverseAlpha, false, false, samples,
|
||||||
shaders: baseShaders,
|
shaders: baseShaders,
|
||||||
shaderName: baseShaderName,
|
shaderName: baseShaderName,
|
||||||
usesRenderPackShaderAbi: usesRenderPackShaderAbi)),
|
|
||||||
Track(CreateMeshPipeline(
|
|
||||||
device,
|
|
||||||
$"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)));
|
usesRenderPackShaderAbi: usesRenderPackShaderAbi)));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
@ -441,10 +415,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
transformBaseInstance);
|
transformBaseInstance);
|
||||||
IGpuBuffer commandBuffer = commands.Buffer;
|
IGpuBuffer commandBuffer = commands.Buffer;
|
||||||
uint commandBase = commands.OffsetBytes;
|
uint commandBase = commands.OffsetBytes;
|
||||||
ReadOnlySpan<DrawElementsIndirectCommand> usedCommands =
|
|
||||||
_indirectCommands.AsSpan(0, totalDraws);
|
|
||||||
ReadOnlySpan<uint> usedDetailCategories =
|
ReadOnlySpan<uint> usedDetailCategories =
|
||||||
_detailCategoryData.AsSpan(0, immediateInstances);
|
_detailCategoryData.AsSpan(0, immediateInstances);
|
||||||
|
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
||||||
|
_buildingDetailEnabled(),
|
||||||
|
_buildingDetail)
|
||||||
|
&& _buildingDetail.Tiling != 0f;
|
||||||
|
|
||||||
// ── Phase 7: opaque pass ─────────────────────────────────────────────
|
// ── Phase 7: opaque pass ─────────────────────────────────────────────
|
||||||
if (_opaqueDrawCount > 0)
|
if (_opaqueDrawCount > 0)
|
||||||
|
|
@ -459,67 +435,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
using (BeginRhiTimer(encoder, diag, OpaqueTimerScope))
|
using (BeginRhiTimer(encoder, diag, OpaqueTimerScope))
|
||||||
{
|
{
|
||||||
DrawIndirectRangeRhi(
|
DrawDetailAwareRangeRhi(
|
||||||
encoder, ref pushConstants, commandBuffer, commandBase,
|
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 ────────────────────────────────────────
|
// ── Phase 8: transparent pass ────────────────────────────────────────
|
||||||
if (_transparentDrawCount > 0)
|
if (_transparentDrawCount > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -538,7 +459,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
ref pushConstants,
|
ref pushConstants,
|
||||||
commandBuffer,
|
commandBuffer,
|
||||||
commandBase,
|
commandBase,
|
||||||
usedDetailCategories);
|
usedDetailCategories,
|
||||||
|
detailEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -594,7 +516,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
/// difference between the two call sites is <paramref name="viewProjection"/>
|
/// difference between the two call sites is <paramref name="viewProjection"/>
|
||||||
/// (the drain reads the deferred scope's own <c>_deferredAlphaViewProjection</c>;
|
/// (the drain reads the deferred scope's own <c>_deferredAlphaViewProjection</c>;
|
||||||
/// an immediate draw uses whatever the CURRENT frame's viewProjection
|
/// 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>
|
/// </summary>
|
||||||
private GpuPushConstants BindAlphaDrawState(
|
private GpuPushConstants BindAlphaDrawState(
|
||||||
IGpuPassEncoder encoder,
|
IGpuPassEncoder encoder,
|
||||||
|
|
@ -703,14 +625,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S4-c2 fix round 1 (M3b): <c>DrawMesh</c> row 1's "render immediately;
|
/// S4-c2 fix round 1 (M3b): <c>DrawMesh</c> row 1's "render immediately;
|
||||||
/// enqueue nothing" outcome for ONE translucent building-shell instance
|
/// enqueue nothing" outcome for ONE translucent building-shell instance
|
||||||
/// under building detail — bind the blend pipeline, one indirect draw,
|
/// under building detail — arm the retail one-pass material on the
|
||||||
/// then the detail pass, reusing <see cref="BindAlphaDrawState"/> and
|
/// original blend pipeline and submit the single slot
|
||||||
/// <see cref="DrawBuildingDetailRangeRhi"/> (the same code
|
/// <see cref="WbDrawDispatcher.DrawImmediateAlphaInstance"/> just
|
||||||
/// <see cref="DrawPreparedAlphaBatchRhi"/> used to use) for the single
|
|
||||||
/// slot <see cref="WbDrawDispatcher.DrawImmediateAlphaInstance"/> just
|
|
||||||
/// prepared. The caller only reaches this method when
|
/// prepared. The caller only reaches this method when
|
||||||
/// <c>detailSurfaceActive</c> was true, so the detail pass always
|
/// <c>detailSurfaceActive</c> was true, so detail always applies here —
|
||||||
/// applies here — the drain's own per-command detail CHECK is gone
|
/// the drain's own per-command detail CHECK is gone
|
||||||
/// (M3c) precisely because detail-eligible content no longer reaches it.
|
/// (M3c) precisely because detail-eligible content no longer reaches it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void DrawImmediateAlphaInstanceRhi(
|
private void DrawImmediateAlphaInstanceRhi(
|
||||||
|
|
@ -726,6 +646,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
|
|
||||||
BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh);
|
BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh);
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
|
ArmBuildingDetail(ref pushConstants);
|
||||||
DrawIndirectRangeRhi(
|
DrawIndirectRangeRhi(
|
||||||
encoder,
|
encoder,
|
||||||
ref pushConstants,
|
ref pushConstants,
|
||||||
|
|
@ -733,16 +654,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
_alphaCommands.OffsetBytes,
|
_alphaCommands.OffsetBytes,
|
||||||
startCommand: 0,
|
startCommand: 0,
|
||||||
commandCount: 1);
|
commandCount: 1);
|
||||||
|
ClearDetailPushConstants(ref pushConstants);
|
||||||
DrawBuildingDetailRangeRhi(
|
encoder.SetPushConstants(in pushConstants);
|
||||||
encoder,
|
|
||||||
mesh,
|
|
||||||
pipelines.RetailDetailTransparent,
|
|
||||||
ref pushConstants,
|
|
||||||
_alphaCommands.Buffer!,
|
|
||||||
_alphaCommands.OffsetBytes,
|
|
||||||
firstCommand: 0,
|
|
||||||
commandCount: 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawImmediateTransparentRhi(
|
private void DrawImmediateTransparentRhi(
|
||||||
|
|
@ -752,21 +665,21 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
ref GpuPushConstants pushConstants,
|
ref GpuPushConstants pushConstants,
|
||||||
IGpuBuffer commandBuffer,
|
IGpuBuffer commandBuffer,
|
||||||
uint commandBase,
|
uint commandBase,
|
||||||
ReadOnlySpan<uint> usedDetailCategories)
|
ReadOnlySpan<uint> usedDetailCategories,
|
||||||
|
bool detailEnabled)
|
||||||
{
|
{
|
||||||
bool detailEnabled = RetailDetailTextureContract.ShouldRender(
|
|
||||||
_buildingDetailEnabled(),
|
|
||||||
_buildingDetail);
|
|
||||||
int command = _opaqueDrawCount;
|
int command = _opaqueDrawCount;
|
||||||
int end = command + _transparentDrawCount;
|
int end = command + _transparentDrawCount;
|
||||||
while (command < end)
|
while (command < end)
|
||||||
{
|
{
|
||||||
|
TranslucencyKind blend = _groupInputScratch[command].Translucency;
|
||||||
bool hasDetail = detailEnabled
|
bool hasDetail = detailEnabled
|
||||||
&& CommandContainsDetailCategory(
|
&& CommandContainsDetailCategory(
|
||||||
_indirectCommands[command],
|
_indirectCommands[command],
|
||||||
usedDetailCategories);
|
usedDetailCategories);
|
||||||
int runEnd = command + 1;
|
int runEnd = command + 1;
|
||||||
while (runEnd < end
|
while (runEnd < end
|
||||||
|
&& _groupInputScratch[runEnd].Translucency == blend
|
||||||
&& !hasDetail
|
&& !hasDetail
|
||||||
&& (!detailEnabled
|
&& (!detailEnabled
|
||||||
|| !CommandContainsDetailCategory(
|
|| !CommandContainsDetailCategory(
|
||||||
|
|
@ -776,8 +689,14 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
runEnd++;
|
runEnd++;
|
||||||
}
|
}
|
||||||
|
|
||||||
BindPipelineWithMesh(encoder, pipelines.AlphaBlend, mesh);
|
BindPipelineWithMesh(
|
||||||
ClearDetailPushConstants(ref pushConstants);
|
encoder,
|
||||||
|
PipelineForBlend(pipelines, blend),
|
||||||
|
mesh);
|
||||||
|
if (hasDetail)
|
||||||
|
ArmBuildingDetail(ref pushConstants);
|
||||||
|
else
|
||||||
|
ClearDetailPushConstants(ref pushConstants);
|
||||||
DrawIndirectRangeRhi(
|
DrawIndirectRangeRhi(
|
||||||
encoder,
|
encoder,
|
||||||
ref pushConstants,
|
ref pushConstants,
|
||||||
|
|
@ -785,45 +704,60 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
commandBase,
|
commandBase,
|
||||||
command,
|
command,
|
||||||
runEnd - command);
|
runEnd - command);
|
||||||
|
|
||||||
if (hasDetail)
|
|
||||||
{
|
|
||||||
DrawBuildingDetailRangeRhi(
|
|
||||||
encoder,
|
|
||||||
mesh,
|
|
||||||
pipelines.RetailDetailTransparent,
|
|
||||||
ref pushConstants,
|
|
||||||
commandBuffer,
|
|
||||||
commandBase,
|
|
||||||
command,
|
|
||||||
1);
|
|
||||||
}
|
|
||||||
command = runEnd;
|
command = runEnd;
|
||||||
}
|
}
|
||||||
|
ClearDetailPushConstants(ref pushConstants);
|
||||||
|
encoder.SetPushConstants(in pushConstants);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawBuildingDetailRangeRhi(
|
private void DrawDetailAwareRangeRhi(
|
||||||
IGpuPassEncoder encoder,
|
IGpuPassEncoder encoder,
|
||||||
GlobalMeshBuffer mesh,
|
|
||||||
IGpuPipeline detailPipeline,
|
|
||||||
ref GpuPushConstants pushConstants,
|
ref GpuPushConstants pushConstants,
|
||||||
IGpuBuffer commandBuffer,
|
IGpuBuffer commandBuffer,
|
||||||
uint commandBase,
|
uint commandBase,
|
||||||
int firstCommand,
|
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.TextureIndexA = _buildingDetail.TextureSlot.Index;
|
||||||
pushConstants.ParamA = _buildingDetail.Tiling;
|
pushConstants.ParamA = _buildingDetail.Tiling;
|
||||||
pushConstants.ParamB = 1f;
|
|
||||||
DrawIndirectRangeRhi(
|
|
||||||
encoder,
|
|
||||||
ref pushConstants,
|
|
||||||
commandBuffer,
|
|
||||||
commandBase,
|
|
||||||
firstCommand,
|
|
||||||
commandCount);
|
|
||||||
ClearDetailPushConstants(ref pushConstants);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ClearDetailPushConstants(
|
private static void ClearDetailPushConstants(
|
||||||
|
|
@ -1139,11 +1073,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
totalMs += transparentMs;
|
totalMs += transparentMs;
|
||||||
any = true;
|
any = true;
|
||||||
}
|
}
|
||||||
if (_device.Timers.TryResolve(DetailTimerScope, out double detailMs))
|
|
||||||
{
|
|
||||||
totalMs += detailMs;
|
|
||||||
any = true;
|
|
||||||
}
|
|
||||||
if (!any)
|
if (!any)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -1174,8 +1103,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
pipelines.AlphaBlend.Dispose();
|
pipelines.AlphaBlend.Dispose();
|
||||||
pipelines.AlphaAdditive.Dispose();
|
pipelines.AlphaAdditive.Dispose();
|
||||||
pipelines.AlphaInverse.Dispose();
|
pipelines.AlphaInverse.Dispose();
|
||||||
pipelines.RetailDetail.Dispose();
|
|
||||||
pipelines.RetailDetailTransparent.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class NullRhiTimerScope : IDisposable
|
private sealed class NullRhiTimerScope : IDisposable
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
batch.FirstIndex, (int)batch.BaseVertex,
|
batch.FirstIndex, (int)batch.BaseVertex,
|
||||||
batch.IndexCount, texture.Slot, texture.Layer, translucency,
|
batch.IndexCount, texture.Slot, texture.Layer, translucency,
|
||||||
FoliageFlags: foliageFlags,
|
FoliageFlags: foliageFlags,
|
||||||
|
SurfaceOpacity: batch.SurfaceOpacity,
|
||||||
CullMode: batch.CullMode);
|
CullMode: batch.CullMode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -617,8 +617,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
private int _transparentDrawCount;
|
private int _transparentDrawCount;
|
||||||
private int _transparentByteOffset;
|
private int _transparentByteOffset;
|
||||||
|
|
||||||
// Campaign V slice V2 (2026-07-27): std430 layout: uint TextureIndex at
|
// std430 layout: uint TextureIndex at offset 0, float SurfaceOpacity at
|
||||||
// offset 0, uint Reserved (pad) at offset 4, uint TextureLayer at offset 8,
|
// offset 4, uint TextureLayer at offset 8,
|
||||||
// uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so
|
// uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so
|
||||||
// every existing CPU writer's offsets are unchanged (see
|
// every existing CPU writer's offsets are unchanged (see
|
||||||
// GpuBindingModel.GpuBatchDataStrideBytes). TextureIndex used to be a
|
// GpuBindingModel.GpuBatchDataStrideBytes). TextureIndex used to be a
|
||||||
|
|
@ -630,7 +630,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
private struct BatchData
|
private struct BatchData
|
||||||
{
|
{
|
||||||
public uint TextureIndex; // slot into the device texture table
|
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 TextureLayer;
|
||||||
public uint Flags;
|
public uint Flags;
|
||||||
}
|
}
|
||||||
|
|
@ -1947,6 +1947,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
_batchData[i] = new BatchData
|
_batchData[i] = new BatchData
|
||||||
{
|
{
|
||||||
TextureIndex = _batchPublicScratch[i].TextureIndex,
|
TextureIndex = _batchPublicScratch[i].TextureIndex,
|
||||||
|
SurfaceOpacity = _batchPublicScratch[i].SurfaceOpacity,
|
||||||
TextureLayer = _batchPublicScratch[i].TextureLayer,
|
TextureLayer = _batchPublicScratch[i].TextureLayer,
|
||||||
Flags = _batchPublicScratch[i].Flags,
|
Flags = _batchPublicScratch[i].Flags,
|
||||||
};
|
};
|
||||||
|
|
@ -2049,6 +2050,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
TextureIndex: g.TextureSlot.Index,
|
TextureIndex: g.TextureSlot.Index,
|
||||||
TextureLayer: g.TextureLayer,
|
TextureLayer: g.TextureLayer,
|
||||||
Translucency: g.Translucency,
|
Translucency: g.Translucency,
|
||||||
|
SurfaceOpacity: g.SurfaceOpacity,
|
||||||
CullMode: g.CullMode,
|
CullMode: g.CullMode,
|
||||||
FoliageFlags: g.FoliageFlags);
|
FoliageFlags: g.FoliageFlags);
|
||||||
|
|
||||||
|
|
@ -2122,6 +2124,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
g.TextureLayer,
|
g.TextureLayer,
|
||||||
g.Translucency,
|
g.Translucency,
|
||||||
g.FoliageFlags,
|
g.FoliageFlags,
|
||||||
|
g.SurfaceOpacity,
|
||||||
g.CullMode);
|
g.CullMode);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -2143,6 +2146,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
Translucency = key.Translucency,
|
Translucency = key.Translucency,
|
||||||
CullMode = key.CullMode,
|
CullMode = key.CullMode,
|
||||||
FoliageFlags = key.FoliageFlags,
|
FoliageFlags = key.FoliageFlags,
|
||||||
|
SurfaceOpacity = key.SurfaceOpacity,
|
||||||
Registration = registration,
|
Registration = registration,
|
||||||
LastUsedFrame = frame,
|
LastUsedFrame = frame,
|
||||||
};
|
};
|
||||||
|
|
@ -2478,7 +2482,8 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
{
|
{
|
||||||
byte mask = RetailAlphaMeshRouter.MaskFromTranslucencyKind(kind);
|
byte mask = RetailAlphaMeshRouter.MaskFromTranslucencyKind(kind);
|
||||||
bool detailSurfaceActive = isBuildingShell
|
bool detailSurfaceActive = isBuildingShell
|
||||||
&& RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _buildingDetail);
|
&& RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _buildingDetail)
|
||||||
|
&& _buildingDetail.Tiling != 0f;
|
||||||
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
|
||||||
currentlyDrawingSky: false,
|
currentlyDrawingSky: false,
|
||||||
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
delayMask: RetailAlphaMeshRouter.DefaultDelayMask,
|
||||||
|
|
@ -2558,11 +2563,12 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
{
|
{
|
||||||
// Campaign V slice V2: table slot, not the raw handle.
|
// Campaign V slice V2: table slot, not the raw handle.
|
||||||
TextureIndex = key.TextureSlot.Index,
|
TextureIndex = key.TextureSlot.Index,
|
||||||
|
SurfaceOpacity = key.SurfaceOpacity,
|
||||||
TextureLayer = key.TextureLayer,
|
TextureLayer = key.TextureLayer,
|
||||||
// DrawMesh invokes RenderMeshSubset with detail enabled for
|
// DrawMesh invokes RenderMeshSubset with detail enabled for
|
||||||
// every built-mesh material subset while curr_detail_surface
|
// every built-mesh material subset while curr_detail_surface
|
||||||
// is installed. The per-instance category still rejects
|
// 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 —
|
// bit 0 (the #226 built-mesh marker) is unconditional here —
|
||||||
// every delayed-alpha entry IS a built-mesh alpha submission
|
// every delayed-alpha entry IS a built-mesh alpha submission
|
||||||
// — but the previous hardcoded `1` silently dropped bits 1/2
|
// — but the previous hardcoded `1` silently dropped bits 1/2
|
||||||
|
|
@ -3431,6 +3437,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
uint TextureIndex,
|
uint TextureIndex,
|
||||||
uint TextureLayer,
|
uint TextureLayer,
|
||||||
TranslucencyKind Translucency,
|
TranslucencyKind Translucency,
|
||||||
|
float SurfaceOpacity = 1f,
|
||||||
CullMode CullMode = CullMode.CounterClockwise,
|
CullMode CullMode = CullMode.CounterClockwise,
|
||||||
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
|
// Campaign VM VM6: FoliageWindClassification.CutoutFoliageFlag /
|
||||||
// TrunkFlag, OR'd into BatchDataPublic.Flags alongside #226's bit 0
|
// 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 struct BatchDataPublic
|
||||||
{
|
{
|
||||||
public uint TextureIndex;
|
public uint TextureIndex;
|
||||||
public uint Reserved;
|
public float SurfaceOpacity;
|
||||||
public uint TextureLayer;
|
public uint TextureLayer;
|
||||||
public uint Flags;
|
public uint Flags;
|
||||||
}
|
}
|
||||||
|
|
@ -3496,7 +3503,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
var bd = new BatchDataPublic
|
var bd = new BatchDataPublic
|
||||||
{
|
{
|
||||||
TextureIndex = g.TextureIndex,
|
TextureIndex = g.TextureIndex,
|
||||||
Reserved = 0,
|
SurfaceOpacity = g.SurfaceOpacity,
|
||||||
TextureLayer = g.TextureLayer,
|
TextureLayer = g.TextureLayer,
|
||||||
// #226: this is the built-mesh path. Retail DrawMesh passes
|
// #226: this is the built-mesh path. Retail DrawMesh passes
|
||||||
// curr_detail_surface through RenderMeshSubset for opaque,
|
// curr_detail_surface through RenderMeshSubset for opaque,
|
||||||
|
|
@ -3668,6 +3675,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
|
AcDream.App.Rendering.Gpu.GpuTextureSlot.Unassigned;
|
||||||
public uint TextureLayer; // Layer in either the pooled composite array or WB shared atlas.
|
public uint TextureLayer; // Layer in either the pooled composite array or WB shared atlas.
|
||||||
public TranslucencyKind Translucency;
|
public TranslucencyKind Translucency;
|
||||||
|
public float SurfaceOpacity = 1f;
|
||||||
public CullMode CullMode;
|
public CullMode CullMode;
|
||||||
public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes)
|
public int FirstInstance; // offset into the shared instance VBO (in instances, not bytes)
|
||||||
public int InstanceCount;
|
public int InstanceCount;
|
||||||
|
|
@ -3717,7 +3725,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
public readonly List<uint> IndoorFlags = new();
|
public readonly List<uint> IndoorFlags = new();
|
||||||
|
|
||||||
// #226: 1 for a building-shell instance, 0 otherwise. Parallel to
|
// #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();
|
public readonly List<uint> DetailCategories = new();
|
||||||
|
|
||||||
// #188: per-instance opacity multiplier, parallel to Matrices.
|
// #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
|
/// <para>Scope: WORLD-SPACE geometry only (terrain, EnvCell shells, entity
|
||||||
/// meshes, particles, portal punches, the directional shadow caster/receiver
|
/// meshes, particles, portal punches, the directional shadow caster/receiver
|
||||||
/// pair). The two <c>RetailDetail</c> pipelines
|
/// pair). Detail-active subsets retain those same base pipeline depth states;
|
||||||
/// (<see cref="RetailDetailTextureContract"/>'s <c>Equal</c>/<c>LessOrEqual</c>
|
/// their material combine occurs inside the original fragment shader.</para>
|
||||||
/// pair, used by the building-detail overlay replay) are a documented
|
|
||||||
/// exception with their own citation and are untouched by this contract.</para>
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class WorldDepthContract
|
internal static class WorldDepthContract
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ internal interface IWorldPassSurface
|
||||||
/// world pass now: <c>portal_depth.vert</c> (the exit seals and punch
|
/// world pass now: <c>portal_depth.vert</c> (the exit seals and punch
|
||||||
/// fans, planes handed per draw through the TerrainClip UBO). S3 review
|
/// fans, planes handed per draw through the TerrainClip UBO). S3 review
|
||||||
/// fix round 1 (F5) deleted the mesh/particle vertex shaders'
|
/// 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>particle</c>, <c>particle_mesh</c>) own per-cell fetch and
|
||||||
/// <c>gl_ClipDistance</c> loop outright: the per-cell clip-region table
|
/// <c>gl_ClipDistance</c> loop outright: the per-cell clip-region table
|
||||||
/// <see cref="PrepareClipFrame"/> used to publish for them had no path
|
/// <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
|
/// Distance are LIVE through S5-c3's shared building-degrade owner. Landscape
|
||||||
/// Draw Distance is LIVE through <c>ApplyLandscapeDrawDistance</c>. Building
|
/// Draw Distance is LIVE through <c>ApplyLandscapeDrawDistance</c>. Building
|
||||||
/// Detail Textures is LIVE: #226 consumes it directly in the retail
|
/// 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
|
/// Camera Stiffness/Adjustment Speed/Align To Slope/Mouse Look
|
||||||
/// Sensitivity/Invert Mouselook Y Axis/Use Mouse Turning (TS-74 — no
|
/// Sensitivity/Invert Mouselook Y Axis/Use Mouse Turning (TS-74 — no
|
||||||
/// persistent mouse-turning camera mode exists for ANY of these six to
|
/// 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:
|
// Every row except Building Detail Textures is still store-only:
|
||||||
// #226 consumes that existing preference in the retail building and
|
// #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(
|
BuildMenuRow(
|
||||||
listBox, "ID_Graphics_LandscapeTextureDetail", TextureDetailChoices, page, resolveString,
|
listBox, "ID_Graphics_LandscapeTextureDetail", TextureDetailChoices, page, resolveString,
|
||||||
read: () => bindings.LoadDisplay().LandscapeTextureDetail,
|
read: () => bindings.LoadDisplay().LandscapeTextureDetail,
|
||||||
|
|
@ -1336,7 +1336,7 @@ public static class ConfigOptionsPageController
|
||||||
listBox, "ID_Graphics_BuildingDetailTextures", defaultValue: true, page, resolveString,
|
listBox, "ID_Graphics_BuildingDetailTextures", defaultValue: true, page, resolveString,
|
||||||
read: () => bindings.LoadDisplay().BuildingDetailTextures,
|
read: () => bindings.LoadDisplay().BuildingDetailTextures,
|
||||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { BuildingDetailTextures = value }),
|
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(
|
BuildToggleRow(
|
||||||
listBox, "ID_Graphics_MultiPassAlpha", defaultValue: false, page, resolveString,
|
listBox, "ID_Graphics_MultiPassAlpha", defaultValue: false, page, resolveString,
|
||||||
|
|
|
||||||
|
|
@ -599,6 +599,10 @@ public sealed class MeshExtractor {
|
||||||
Translucency =
|
Translucency =
|
||||||
TranslucencyKindExtensions.FromSurfaceType(
|
TranslucencyKindExtensions.FromSurfaceType(
|
||||||
surface.Type),
|
surface.Type),
|
||||||
|
SurfaceOpacity =
|
||||||
|
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
|
||||||
|
surface.Type,
|
||||||
|
surface.Translucency),
|
||||||
IsTransparent = isTransparent,
|
IsTransparent = isTransparent,
|
||||||
IsAdditive = isAdditive
|
IsAdditive = isAdditive
|
||||||
};
|
};
|
||||||
|
|
@ -1072,6 +1076,10 @@ public sealed class MeshExtractor {
|
||||||
UploadPixelFormat = uploadPixelFormat,
|
UploadPixelFormat = uploadPixelFormat,
|
||||||
UploadPixelType = uploadPixelType,
|
UploadPixelType = uploadPixelType,
|
||||||
Translucency = TranslucencyKindExtensions.FromSurfaceType(surface.Type),
|
Translucency = TranslucencyKindExtensions.FromSurfaceType(surface.Type),
|
||||||
|
SurfaceOpacity =
|
||||||
|
TranslucencyKindExtensions.OpacityFromSurfaceTranslucency(
|
||||||
|
surface.Type,
|
||||||
|
surface.Translucency),
|
||||||
IsTransparent = isTransparent,
|
IsTransparent = isTransparent,
|
||||||
IsAdditive = isAdditive,
|
IsAdditive = isAdditive,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,13 @@ public class TextureBatchData {
|
||||||
public bool IsAdditive { get; set; }
|
public bool IsAdditive { get; set; }
|
||||||
public bool HasWrappingUVs { 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>
|
/// <summary>
|
||||||
/// Retail source surface-array index this subset was constructed from
|
/// Retail source surface-array index this subset was constructed from
|
||||||
/// (contract §3.6) — the CellStruct subset/material OWNER, not the
|
/// (contract §3.6) — the CellStruct subset/material OWNER, not the
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,9 @@ public static class ObjectMeshDataSerializer {
|
||||||
w.Write(batch.RetailSurfaceMask);
|
w.Write(batch.RetailSurfaceMask);
|
||||||
w.Write(batch.RawSurfaceType);
|
w.Write(batch.RawSurfaceType);
|
||||||
w.Write(batch.IsCellShell);
|
w.Write(batch.IsCellShell);
|
||||||
|
// OVERHAUL S5-c4: recipe 9 appends authored material opacity. Recipe
|
||||||
|
// identity rejects older payloads before this serializer is entered.
|
||||||
|
w.Write(batch.SurfaceOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TextureBatchData ReadTextureBatchData(
|
private static TextureBatchData ReadTextureBatchData(
|
||||||
|
|
@ -252,6 +255,7 @@ public static class ObjectMeshDataSerializer {
|
||||||
batch.RetailSurfaceMask = r.ReadByte();
|
batch.RetailSurfaceMask = r.ReadByte();
|
||||||
batch.RawSurfaceType = r.ReadUInt32();
|
batch.RawSurfaceType = r.ReadUInt32();
|
||||||
batch.IsCellShell = r.ReadBoolean();
|
batch.IsCellShell = r.ReadBoolean();
|
||||||
|
batch.SurfaceOpacity = r.ReadSingle();
|
||||||
return batch;
|
return batch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,11 @@ public static class PakFormat {
|
||||||
/// source surface-array index rather than TextureKey/stippling/sides,
|
/// source surface-array index rather than TextureKey/stippling/sides,
|
||||||
/// and built-EnvCell admission is
|
/// and built-EnvCell admission is
|
||||||
/// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after
|
/// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after
|
||||||
/// surface resolution. The binary format remains version 2, but every
|
/// surface resolution. Version 9 appends authored surface opacity to
|
||||||
/// prepared CellStruct/EnvCell render record must be regenerated.
|
/// every prepared mesh texture batch. The binary format remains version
|
||||||
|
/// 2, but every prepared render record must be regenerated.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const uint CurrentBakeToolVersion = 8;
|
public const uint CurrentBakeToolVersion = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,10 @@ public static class ContentMigrationCatalog
|
||||||
7,
|
7,
|
||||||
8,
|
8,
|
||||||
"exact CellStruct surface-index subset construction"),
|
"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)
|
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
|
// Version 7 keeps pak format 2 and regenerates every GfxObj render record
|
||||||
// with retail's authored DrawingBSP view sphere. Version 8 (OH2/S1) keeps
|
// with retail's authored DrawingBSP view sphere. Version 8 (OH2/S1) keeps
|
||||||
// pak format 2 and regenerates every CellStruct/EnvCell render record
|
// pak format 2 and regenerates every CellStruct/EnvCell render record
|
||||||
// with retail's exact surface-array-index subset construction. Both are
|
// with retail's exact surface-array-index subset construction. Version 9
|
||||||
// mandatory full rebuilds from their predecessor recipe.
|
// carries authored surface opacity in prepared mesh batches. Each is a
|
||||||
public const uint CurrentBakeToolVersion = 8;
|
// mandatory full rebuild from its predecessor recipe.
|
||||||
|
public const uint CurrentBakeToolVersion = 9;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -90,11 +90,12 @@ public static class RenderPackSpirvValidator
|
||||||
new([0u, 1u], [], true, true, true, false, false),
|
new([0u, 1u], [], true, true, true, false, false),
|
||||||
RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver =>
|
RenderPipelineVariantSemantic.TerrainDirectionalShadowReceiver =>
|
||||||
new([], [1u, 2u, 3u], true, false, true, false, true),
|
new([], [1u, 2u, 3u], true, false, true, false, true),
|
||||||
// Campaign VM VM6: mesh_atmospheric.vert now reads
|
// Campaign VM VM6: mesh_atmospheric.vert reads the atmospheric
|
||||||
// uAtmosphereClockWind/uAtmosphereWindAmplitude for the same
|
// clock/wind inputs for the same foliage displacement the casters
|
||||||
// foliage-wind displacement the casters apply.
|
// apply. Campaign OVERHAUL S5-c4 adds binding 9 for the per-instance
|
||||||
|
// detail category used by the retail one-pass material combine.
|
||||||
RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver =>
|
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),
|
true, true, true, false, true),
|
||||||
_ => new(
|
_ => new(
|
||||||
[],
|
[],
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ public sealed class DirectionalShadowReceiverTests
|
||||||
Assert.Contains("instanceIndoor[instanceIndex]", vertex, StringComparison.Ordinal);
|
Assert.Contains("instanceIndoor[instanceIndex]", vertex, StringComparison.Ordinal);
|
||||||
Assert.Contains("vSelectionLighting", fragment, StringComparison.Ordinal);
|
Assert.Contains("vSelectionLighting", fragment, StringComparison.Ordinal);
|
||||||
Assert.Contains("vOpacityMultiplier", fragment, StringComparison.Ordinal);
|
Assert.Contains("vOpacityMultiplier", fragment, StringComparison.Ordinal);
|
||||||
Assert.Contains("if (color.a < 0.05) discard", fragment, StringComparison.Ordinal);
|
Assert.Contains("color.a < alphaCutoff", fragment, StringComparison.Ordinal);
|
||||||
Assert.Contains("ACDREAM_SAMPLE_ARRAY", fragment, StringComparison.Ordinal);
|
Assert.Contains("ACDREAM_SAMPLE_ARRAY", fragment, StringComparison.Ordinal);
|
||||||
Assert.Contains("combineOverlays", terrain, StringComparison.Ordinal);
|
Assert.Contains("combineOverlays", terrain, StringComparison.Ordinal);
|
||||||
Assert.Contains("combineRoad", terrain, StringComparison.Ordinal);
|
Assert.Contains("combineRoad", terrain, StringComparison.Ordinal);
|
||||||
|
|
@ -115,12 +115,13 @@ public sealed class DirectionalShadowReceiverTests
|
||||||
Assert.Contains("radius = clamp(radius, 0, 2)", receiver, StringComparison.Ordinal);
|
Assert.Contains("radius = clamp(radius, 0, 2)", receiver, StringComparison.Ordinal);
|
||||||
Assert.Contains("float softness = max(uShadowControl.y, 1.0)", receiver, StringComparison.Ordinal);
|
Assert.Contains("float softness = max(uShadowControl.y, 1.0)", receiver, StringComparison.Ordinal);
|
||||||
|
|
||||||
string detailVertex = File.ReadAllText(Path.Combine(
|
foreach (string worldVertexName in new[] { "mesh_modern.vert", "mesh_atmospheric.vert" })
|
||||||
shaderRoot,
|
{
|
||||||
"mesh_detail.vert"));
|
string worldVertex = File.ReadAllText(Path.Combine(shaderRoot, worldVertexName));
|
||||||
Assert.Contains("int instanceIndex = transformIndex - int(uTextureIndexB)", detailVertex, StringComparison.Ordinal);
|
Assert.Contains("Instances[", worldVertex, StringComparison.Ordinal);
|
||||||
Assert.Contains("Instances[transformIndex].transform", detailVertex, StringComparison.Ordinal);
|
Assert.Contains("].transform", worldVertex, StringComparison.Ordinal);
|
||||||
Assert.Contains("instanceDetailCategory[instanceIndex]", detailVertex, StringComparison.Ordinal);
|
Assert.Contains("instanceDetailCategory[instanceIndex]", worldVertex, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RepositoryRoot()
|
private static string RepositoryRoot()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Gpu;
|
using AcDream.App.Rendering.Gpu;
|
||||||
using AcDream.App.Rendering.Gpu.Vk;
|
using AcDream.App.Rendering.Gpu.Vk;
|
||||||
|
|
@ -74,34 +75,53 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
DrawPipelineNames(fixture.Device));
|
DrawPipelineNames(fixture.Device));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public void DetailOn_DrawsAtCellTurnWithDetail_AndQueuesNothing()
|
[InlineData(0, "envcell-opaque", 0f)]
|
||||||
|
[InlineData(1, "envcell-alpha", 0f)]
|
||||||
|
[InlineData(2, "envcell-additive", 0f)]
|
||||||
|
[InlineData(3, "envcell-clip", 200f / 255f)]
|
||||||
|
[InlineData(4, "envcell-clip", 100f / 255f)]
|
||||||
|
public void DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity(
|
||||||
|
int specIndex,
|
||||||
|
string expectedPipeline,
|
||||||
|
float expectedReference)
|
||||||
{
|
{
|
||||||
|
BatchSpec spec = specIndex switch
|
||||||
|
{
|
||||||
|
0 => BatchSpec.Opaque,
|
||||||
|
1 => BatchSpec.Alpha,
|
||||||
|
2 => BatchSpec.Additive,
|
||||||
|
3 => BatchSpec.ClipDds,
|
||||||
|
4 => BatchSpec.ClipPaletted,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(specIndex)),
|
||||||
|
};
|
||||||
using var fixture = new ProductionEnvCellFixture(
|
using var fixture = new ProductionEnvCellFixture(
|
||||||
detailSurfaceActive: true,
|
detailSurfaceActive: true,
|
||||||
BatchSpec.ClipDds);
|
spec);
|
||||||
fixture.Queue.BeginFrame();
|
fixture.Queue.BeginFrame();
|
||||||
|
|
||||||
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
|
||||||
|
|
||||||
Assert.Equal(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
["envcell-clip", "envcell-retail-detail-clip"],
|
[expectedPipeline],
|
||||||
DrawPipelineNames(fixture.Device));
|
DrawPipelineNames(fixture.Device));
|
||||||
Assert.All(
|
Assert.All(
|
||||||
DrawPushConstants(fixture.Device),
|
DrawPushConstants(fixture.Device),
|
||||||
constants => Assert.Equal(200f / 255f, constants.ParamB));
|
constants => Assert.Equal(expectedReference, constants.ParamB));
|
||||||
GpuPipelineDescription detailPipeline = fixture.Device.CreatedPipelines
|
GpuPushConstants armed = Assert.Single(
|
||||||
.Single(static pipeline =>
|
DrawPushConstants(fixture.Device),
|
||||||
pipeline.Description.Name == "envcell-retail-detail-clip")
|
constants => constants.ParamA != 0f);
|
||||||
.Description;
|
Assert.NotEqual(0u, armed.TextureIndexA);
|
||||||
Assert.Equal(GpuBlendMode.RetailDetail, detailPipeline.Blend);
|
|
||||||
Assert.True(detailPipeline.Depth.Test);
|
|
||||||
Assert.True(detailPipeline.Depth.Write);
|
|
||||||
Assert.Equal(GpuCompareOp.Equal, detailPipeline.Depth.Compare);
|
|
||||||
Assert.Equal(0, fixture.Queue.PendingCount);
|
Assert.Equal(0, fixture.Queue.PendingCount);
|
||||||
|
GpuRecordedStorageBind batchBind = fixture.Device.Calls
|
||||||
|
.OfType<GpuRecordedStorageBind>()
|
||||||
|
.Last(call => call.Binding == GpuBindingModel.StorageBatches);
|
||||||
|
ReadOnlySpan<ModernBatchData> gpuBatches = MemoryMarshal.Cast<byte, ModernBatchData>(
|
||||||
|
fixture.Device.RingBytes.Slice((int)batchBind.OffsetBytes, (int)batchBind.SizeBytes));
|
||||||
|
Assert.Equal(0.25f, Assert.Single(gpuBatches.ToArray()).SurfaceOpacity);
|
||||||
fixture.Queue.EndFrame();
|
fixture.Queue.EndFrame();
|
||||||
Assert.Equal(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
|
||||||
|
|
@ -232,28 +252,23 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
string root = RepositoryRoot();
|
string root = RepositoryRoot();
|
||||||
string modern = File.ReadAllText(Path.Combine(
|
string modern = File.ReadAllText(Path.Combine(
|
||||||
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_modern.frag"));
|
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_modern.frag"));
|
||||||
string detail = File.ReadAllText(Path.Combine(
|
string atmospheric = File.ReadAllText(Path.Combine(
|
||||||
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_detail.frag"));
|
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_atmospheric.frag"));
|
||||||
string detailVertex = File.ReadAllText(Path.Combine(
|
|
||||||
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_detail.vert"));
|
|
||||||
|
|
||||||
Assert.Contains("if (color.a < alphaCutoff) discard;", modern, StringComparison.Ordinal);
|
foreach (string shader in new[] { modern, atmospheric })
|
||||||
Assert.DoesNotContain("if (color.a <= alphaCutoff) discard;", modern, StringComparison.Ordinal);
|
|
||||||
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", modern, StringComparison.Ordinal);
|
|
||||||
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", detail, StringComparison.Ordinal);
|
|
||||||
foreach (string shader in new[] { modern, detail })
|
|
||||||
{
|
{
|
||||||
|
Assert.Contains(
|
||||||
|
"if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)",
|
||||||
|
shader,
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("alpha <= alphaCutoff", shader, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("color.a <= alphaCutoff", shader, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", shader, StringComparison.Ordinal);
|
||||||
Assert.Contains("abs(value - (100.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
Assert.Contains("abs(value - (100.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
||||||
Assert.Contains("abs(value - (200.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
Assert.Contains("abs(value - (200.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("uParamB > 0.0", shader, StringComparison.Ordinal);
|
Assert.DoesNotContain("uParamB > 0.0", shader, StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain("value - 0.5", shader, StringComparison.Ordinal);
|
Assert.DoesNotContain("value - 0.5", shader, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
Assert.Contains("if (base.a < alphaCutoff)", detail, StringComparison.Ordinal);
|
|
||||||
Assert.DoesNotContain("if (base.a <= alphaCutoff)", detail, StringComparison.Ordinal);
|
|
||||||
Assert.Contains(
|
|
||||||
"vDetailCategory = uParamB == 1.0",
|
|
||||||
detailVertex,
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -438,6 +453,12 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
IsTransparent: true,
|
IsTransparent: true,
|
||||||
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
|
||||||
PaletteId: 0);
|
PaletteId: 0);
|
||||||
|
|
||||||
|
internal static BatchSpec Additive { get; } = new(
|
||||||
|
RetailAlphaMeshRouter.MaskAlphaFamily,
|
||||||
|
IsTransparent: true,
|
||||||
|
AcDream.Core.Meshing.TranslucencyKind.Additive,
|
||||||
|
PaletteId: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -501,7 +522,9 @@ public sealed class EnvCellAlphaDrawSourceTests
|
||||||
TextureData = new byte[8 * 8 * 4],
|
TextureData = new byte[8 * 8 * 4],
|
||||||
Indices = [0, 1, 2],
|
Indices = [0, 1, 2],
|
||||||
IsTransparent = spec.IsTransparent,
|
IsTransparent = spec.IsTransparent,
|
||||||
|
IsAdditive = spec.Translucency == AcDream.Core.Meshing.TranslucencyKind.Additive,
|
||||||
Translucency = spec.Translucency,
|
Translucency = spec.Translucency,
|
||||||
|
SurfaceOpacity = 0.25f,
|
||||||
RetailSurfaceMask = spec.Mask,
|
RetailSurfaceMask = spec.Mask,
|
||||||
CullMode = CullMode.Clockwise,
|
CullMode = CullMode.Clockwise,
|
||||||
IsCellShell = true,
|
IsCellShell = true,
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,8 @@ public sealed class GpuContractTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StorageBindingsMatchTheShaderSources()
|
public void StorageBindingsMatchTheShaderSources()
|
||||||
{
|
{
|
||||||
// mesh_modern.vert declares std430 bindings 0..8 in exactly this order;
|
// Both world vertex families declare std430 bindings 0..9 in exactly
|
||||||
// #226's mesh_detail.vert additionally declares binding 9 for the
|
// this order; binding 9 carries per-instance detail eligibility.
|
||||||
// per-instance building category.
|
|
||||||
Assert.Equal(0u, GpuBindingModel.StorageInstances);
|
Assert.Equal(0u, GpuBindingModel.StorageInstances);
|
||||||
Assert.Equal(1u, GpuBindingModel.StorageBatches);
|
Assert.Equal(1u, GpuBindingModel.StorageBatches);
|
||||||
Assert.Equal(2u, GpuBindingModel.StorageClipRegions);
|
Assert.Equal(2u, GpuBindingModel.StorageClipRegions);
|
||||||
|
|
@ -79,15 +78,14 @@ public sealed class GpuContractTests
|
||||||
// WbDrawDispatcher.ApplyRetailBlend selects a blend function from each DAT
|
// WbDrawDispatcher.ApplyRetailBlend selects a blend function from each DAT
|
||||||
// surface's TranslucencyKind. Retail also selects ONE/INVSRCALPHA for a
|
// surface's TranslucencyKind. Retail also selects ONE/INVSRCALPHA for a
|
||||||
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
|
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
|
||||||
// the detail combine is a separate renderer state. Collapsing any of
|
// detail is now computed inside those same pipelines. Collapsing any of
|
||||||
// these modes changes the corresponding subset's composition.
|
// these modes changes the corresponding subset's composition.
|
||||||
Assert.Equal(6, Enum.GetValues<GpuBlendMode>().Length);
|
Assert.Equal(5, Enum.GetValues<GpuBlendMode>().Length);
|
||||||
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
||||||
Assert.Contains(GpuBlendMode.RetailDetail, Enum.GetValues<GpuBlendMode>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -41,23 +41,15 @@ public sealed class VulkanShaderManifestTests
|
||||||
{
|
{
|
||||||
["debug_line.frag.spv"] = "02fc04880bc5eb74353566f914675244038125c71443964decdc28e8199264df",
|
["debug_line.frag.spv"] = "02fc04880bc5eb74353566f914675244038125c71443964decdc28e8199264df",
|
||||||
["debug_line.vert.spv"] = "f9c6a9b575bb07a426fb6ade677bca96a7752ca6b120e8f6451363ba73b51140",
|
["debug_line.vert.spv"] = "f9c6a9b575bb07a426fb6ade677bca96a7752ca6b120e8f6451363ba73b51140",
|
||||||
// Re-pinned 2026-09-04 (Campaign OVERHAUL S4-c2 owner-authorized
|
// Re-pinned 2026-09-04 (Campaign OVERHAUL S5-c4): retail's
|
||||||
// repair): Vulkan expresses SetSurface's pure-ClipMap alpha test
|
// single-pass building detail material now runs inside the base
|
||||||
// through uParamB, recognizing only the paletted 100/255 and DDS
|
// fragment. Final alpha is authoredOpacity*liveFade*detail.a^2;
|
||||||
// 200/255 references and using discard-on-less so equality passes.
|
// CLIP tests that value while detail-off retains base-alpha logic.
|
||||||
// The paired binary selects those globals and
|
["mesh_modern.frag.spv"] = "576af229b96a2a5e9b3b1ff16ab686d96578c2d9d1762b0e225df31bfd7aa86f",
|
||||||
// D3DCMP_GREATEREQUAL at 0x0059c73c..0x0059c838.
|
// S5-c4 replaces the padding word at batch offset 4 with authored
|
||||||
["mesh_modern.frag.spv"] = "1b3a59d0cc7c5bb6acf793d57d9673d860a7eb50bdd51770781f59feac4604a1",
|
// surface opacity and forwards it plus exact detail category/flags
|
||||||
// mesh_modern.vert re-pinned 2026-09-03 (S3 review fix round 1,
|
// flat to the fragment shader. The 16-byte ABI is unchanged.
|
||||||
// F5): the binding=2 ClipRegionBuf (CellClip) SSBO, its
|
["mesh_modern.vert.spv"] = "1ff1b7118153ba0fddfcdfca39bd973a4b8f6b46db6db772e9886061d15561d8",
|
||||||
// gl_ClipDistance loop, and the gl_PerVertex redeclaration are
|
|
||||||
// deleted outright — the ONE WbDrawDispatcher API that could
|
|
||||||
// ever arm per-instance clip routing had ZERO production
|
|
||||||
// callers, so every instance has always mapped to slot
|
|
||||||
// 0 (no-clip) in every shipped build; the shader-side clip test
|
|
||||||
// clipped nothing. binding=3 instanceClipSlot[] stays declared,
|
|
||||||
// unread (fed 0; the instance-buffer layout is S5's).
|
|
||||||
["mesh_modern.vert.spv"] = "f9ed4ee7140ccd136130559dbea68545f733f6022f52b085b6df4bcb787223c6",
|
|
||||||
["particle.frag.spv"] = "680da227704e0b3afa9b5226a7d73dd65aa9d8759d081cf4d5009d30e148726b",
|
["particle.frag.spv"] = "680da227704e0b3afa9b5226a7d73dd65aa9d8759d081cf4d5009d30e148726b",
|
||||||
// Re-pinned 2026-08-27: portal-view clip slots now travel with
|
// Re-pinned 2026-08-27: portal-view clip slots now travel with
|
||||||
// deferred billboard particles, matching retail PortalList draws.
|
// deferred billboard particles, matching retail PortalList draws.
|
||||||
|
|
|
||||||
|
|
@ -149,12 +149,6 @@ public sealed class VulkanViewportMappingTests
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
(BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
(BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAlpha));
|
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAlpha));
|
||||||
// VM2 (2026-08-22): real hardware runs the single-pass detail combine,
|
|
||||||
// a lerp expressed as the ordinary straight-alpha-over factor pair —
|
|
||||||
// not the two-pass DstColor fallback.
|
|
||||||
Assert.Equal(
|
|
||||||
(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
|
||||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RetailDetail));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,6 @@ public sealed class AtmosphericShaderAbiTests
|
||||||
[InlineData("terrain_atmospheric.vert")]
|
[InlineData("terrain_atmospheric.vert")]
|
||||||
[InlineData("directional_shadow_terrain.vert")]
|
[InlineData("directional_shadow_terrain.vert")]
|
||||||
[InlineData("directional_shadow_terrain_multiview.vert")]
|
[InlineData("directional_shadow_terrain_multiview.vert")]
|
||||||
[InlineData("mesh_detail.vert")]
|
|
||||||
public void RetailAndTerrainShadersNeverCallAcdreamFoliageDisplace(string fileName)
|
public void RetailAndTerrainShadersNeverCallAcdreamFoliageDisplace(string fileName)
|
||||||
{
|
{
|
||||||
string text = File.ReadAllText(Path.Combine(
|
string text = File.ReadAllText(Path.Combine(
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,23 @@ public sealed class RenderPackSpirvValidatorTests
|
||||||
Assert.True(result.Success, result.Reason);
|
Assert.True(result.Success, result.Reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WorldDirectionalShadowReceiver_AllowsOnePassDetailCategoryBinding()
|
||||||
|
{
|
||||||
|
PipelineVariantDeclaration variant = BuiltInAtmosphericRenderPack.Descriptor
|
||||||
|
.PipelineVariants.Single(static value =>
|
||||||
|
value.Semantic
|
||||||
|
== RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver);
|
||||||
|
|
||||||
|
RenderPackSpirvValidationResult result =
|
||||||
|
RenderPackSpirvValidator.ValidatePipelineVariantShader(
|
||||||
|
Shader("mesh_atmospheric.vert.spv"),
|
||||||
|
RenderPackShaderStage.Vertex,
|
||||||
|
variant);
|
||||||
|
|
||||||
|
Assert.True(result.Success, result.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DirectionalShadowUniformRequiresSixMembersAndSelectedSourceAtOffset320()
|
public void DirectionalShadowUniformRequiresSixMembersAndSelectedSourceAtOffset320()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,148 +1,339 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Gpu;
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
|
using DatReaderWriter.Enums;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
public sealed class RetailDetailTextureContractTests
|
public sealed class RetailDetailTextureContractTests
|
||||||
{
|
{
|
||||||
private static readonly TerrainAtlas.RetailDetailTextureBinding Available = new(
|
private static readonly Vector4 Base = new(0.31f, 0.57f, 0.83f, 0.19f);
|
||||||
new GpuTextureSlot(7),
|
private static readonly Vector3 Diffuse = new(0.73f, 0.41f, 0.67f);
|
||||||
Tiling: 4f,
|
private static readonly Vector4 Detail = new(0.91f, 0.23f, 0.49f, 0.62f);
|
||||||
SurfaceTextureId: 0x05001787,
|
private static readonly Vector4 Destination = new(0.17f, 0.37f, 0.71f, 0.29f);
|
||||||
RenderSurfaceId: 0x06006D58,
|
|
||||||
Width: 256,
|
|
||||||
Height: 256);
|
|
||||||
|
|
||||||
[Fact]
|
public static TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float> PolicyRows()
|
||||||
public void ExistingBuildingDetailSettingIsTheLivePassGate()
|
|
||||||
{
|
{
|
||||||
Assert.False(RetailDetailTextureContract.ShouldRender(false, Available));
|
var rows = new TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float>();
|
||||||
Assert.True(RetailDetailTextureContract.ShouldRender(true, Available));
|
(SurfaceType Type, bool Paletted, TranslucencyKind Kind,
|
||||||
Assert.False(RetailDetailTextureContract.ShouldRender(true, default));
|
RetailDetailTextureContract.FramebufferFamily Family, float Reference)[] materials =
|
||||||
}
|
[
|
||||||
|
(SurfaceType.Base1Image, false, TranslucencyKind.Opaque,
|
||||||
[Fact]
|
RetailDetailTextureContract.FramebufferFamily.Opaque, 0.05f),
|
||||||
public void OpaqueDetailUsesDepthEqualityWhileTransparentDetailDoesNot()
|
(SurfaceType.Base1Image | SurfaceType.Alpha, false, TranslucencyKind.AlphaBlend,
|
||||||
{
|
RetailDetailTextureContract.FramebufferFamily.Alpha, 0.05f),
|
||||||
Assert.Equal(
|
(SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false,
|
||||||
GpuCompareOp.Equal,
|
TranslucencyKind.Additive,
|
||||||
RetailDetailTextureContract.DetailDepthCompare(transparent: false));
|
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive, 0.05f),
|
||||||
Assert.Equal(
|
(SurfaceType.Base1Image | SurfaceType.InvAlpha, false, TranslucencyKind.InvAlpha,
|
||||||
GpuCompareOp.LessOrEqual,
|
RetailDetailTextureContract.FramebufferFamily.InverseAlpha, 0.05f),
|
||||||
RetailDetailTextureContract.DetailDepthCompare(transparent: true));
|
(SurfaceType.Base1Image | SurfaceType.Base1ClipMap, false, TranslucencyKind.ClipMap,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Clip, 200f / 255f),
|
||||||
|
(SurfaceType.Base1Image | SurfaceType.Base1ClipMap, true, TranslucencyKind.ClipMap,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Clip, 100f / 255f),
|
||||||
|
];
|
||||||
|
foreach (string consumer in new[] { "Building", "EnvCell" })
|
||||||
|
foreach (var material in materials)
|
||||||
|
foreach (bool detailEnabled in new[] { false, true })
|
||||||
|
{
|
||||||
|
RetailDetailTextureContract.FramebufferFamily family =
|
||||||
|
consumer == "Building" && material.Kind == TranslucencyKind.ClipMap
|
||||||
|
? RetailDetailTextureContract.FramebufferFamily.Opaque
|
||||||
|
: material.Family;
|
||||||
|
rows.Add(consumer, material.Type, material.Paletted, detailEnabled,
|
||||||
|
material.Kind, (int)family, material.Reference);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0.1f, 0.2f, 0.3f)]
|
[MemberData(nameof(PolicyRows))]
|
||||||
[InlineData(0.0f, 0.0f, 0.0f)]
|
public void BuildingAndEnvCellPolicyTable_KeepsOneDrawAndOriginalFramebufferFamily(
|
||||||
[InlineData(1.0f, 1.0f, 1.0f)]
|
string consumer,
|
||||||
[InlineData(0.5f, 0.9f, 0.05f)]
|
SurfaceType surfaceType,
|
||||||
public void ZeroDetailAlphaIsExactNoOp(float r, float g, float b)
|
bool paletted,
|
||||||
|
bool detailEnabled,
|
||||||
|
TranslucencyKind expectedKind,
|
||||||
|
int familyValue,
|
||||||
|
float expectedReference)
|
||||||
{
|
{
|
||||||
var baseColour = new Vector3(r, g, b);
|
var family = (RetailDetailTextureContract.FramebufferFamily)familyValue;
|
||||||
var transparentDetail = new Vector4(0.9f, 0.1f, 0.7f, 0f);
|
Assert.True(consumer is "Building" or "EnvCell");
|
||||||
|
Assert.Equal(expectedKind, TranslucencyKindExtensions.FromSurfaceType(surfaceType));
|
||||||
|
Assert.Equal(["source-subset"], PhysicalDrawSequence(detailEnabled));
|
||||||
|
|
||||||
|
PipelineState state = ExpectedPipelineState(
|
||||||
|
consumer, expectedKind, paletted, expectedReference);
|
||||||
|
Assert.Equal(expectedReference, state.AlphaReference);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
baseColour,
|
expectedKind == TranslucencyKind.ClipMap
|
||||||
RetailDetailTextureContract.Expected(baseColour, transparentDetail, opacity: 1f));
|
? paletted ? 100f / 255f : 200f / 255f
|
||||||
Assert.True(RetailDetailTextureContract.IsNeutral(transparentDetail, opacity: 1f));
|
: 0.05f,
|
||||||
}
|
state.AlphaReference);
|
||||||
|
Assert.True(state.AlphaTest);
|
||||||
[Fact]
|
Assert.Equal(expectedKind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
|
||||||
public void ZeroOpacityIsExactNoOp()
|
state.DepthWrite);
|
||||||
{
|
Assert.Equal(expectedKind switch
|
||||||
var baseColour = new Vector3(0.5f, 0.5f, 0.5f);
|
|
||||||
var derethCategory = new Vector4(0.165f, 0.165f, 0.165f, 0.132f);
|
|
||||||
|
|
||||||
// opacity 0 models a translucency fade that has finished disappearing:
|
|
||||||
// even a fully-opaque detail texel contributes nothing.
|
|
||||||
Assert.Equal(
|
|
||||||
baseColour,
|
|
||||||
RetailDetailTextureContract.Expected(baseColour, derethCategory, opacity: 0f));
|
|
||||||
Assert.True(RetailDetailTextureContract.IsNeutral(derethCategory, opacity: 0f));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void LiveDerethCategoryTextureDarkensMidtones()
|
|
||||||
{
|
|
||||||
// Live category texture 0x06006D58 (VM2), opaque diffuse (opacity 1).
|
|
||||||
var detail = new Vector4(0.165f, 0.165f, 0.161f, 0.132f);
|
|
||||||
var baseColour = new Vector3(0.5f, 0.5f, 0.5f);
|
|
||||||
|
|
||||||
Vector3 result = RetailDetailTextureContract.Expected(baseColour, detail, opacity: 1f);
|
|
||||||
|
|
||||||
Assert.True(result.X < baseColour.X);
|
|
||||||
Assert.True(result.Y < baseColour.Y);
|
|
||||||
Assert.True(result.Z < baseColour.Z);
|
|
||||||
|
|
||||||
// lerp(base, detail.rgb, 0.132) ~= 0.868 * base + 0.0218 per channel —
|
|
||||||
// the mild darkening VM2 measured, not the two-pass fallback's
|
|
||||||
// brightening. Compared as an absolute difference (not xunit's
|
|
||||||
// decimal-rounding `precision` parameter, which would flip a channel
|
|
||||||
// sitting on a rounding boundary) against the stated 1e-3 tolerance.
|
|
||||||
float approx = 0.868f * baseColour.X + 0.0218f; // same for every channel: baseColour is uniform
|
|
||||||
Assert.True(MathF.Abs(result.X - approx) < 1e-3f, $"R off by {result.X - approx}");
|
|
||||||
Assert.True(MathF.Abs(result.Y - approx) < 1e-3f, $"G off by {result.Y - approx}");
|
|
||||||
Assert.True(MathF.Abs(result.Z - approx) < 1e-3f, $"B off by {result.Z - approx}");
|
|
||||||
Assert.False(RetailDetailTextureContract.IsNeutral(detail, opacity: 1f));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FullAlphaReplacesBaseWithDetailColour()
|
|
||||||
{
|
|
||||||
var baseColour = new Vector3(0.9f, 0.1f, 0.4f);
|
|
||||||
var detail = new Vector4(0.2f, 0.3f, 0.4f, 1f);
|
|
||||||
|
|
||||||
Vector3 result = RetailDetailTextureContract.Expected(baseColour, detail, opacity: 1f);
|
|
||||||
|
|
||||||
// Tolerance rather than bit-exact equality: Lerp's `a + (b - a) * t`
|
|
||||||
// form is not guaranteed to cancel `a` perfectly at t=1 in floating
|
|
||||||
// point for arbitrary inputs.
|
|
||||||
Assert.True(MathF.Abs(result.X - detail.X) < 1e-6f);
|
|
||||||
Assert.True(MathF.Abs(result.Y - detail.Y) < 1e-6f);
|
|
||||||
Assert.True(MathF.Abs(result.Z - detail.Z) < 1e-6f);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FogAppliedPerDrawMatchesRetailsFogAfterCombineOrder()
|
|
||||||
{
|
|
||||||
// Review fix (post-05970306): retail fogs the pixel AFTER the
|
|
||||||
// texture-stage combine (mix(lerp(base,detail,a), fog, f)), but
|
|
||||||
// acdream draws the combine as two separate passes, each fogging its
|
|
||||||
// own colour before the fixed-function blend recombines them
|
|
||||||
// (lerp(mix(base,fog,f), mix(detail,fog,f), a)). This pins that the
|
|
||||||
// two orders are the same pixel for arbitrary inputs — see
|
|
||||||
// ExpectedFogged's doc comment and mesh_detail.frag's header comment
|
|
||||||
// for the algebra.
|
|
||||||
var random = new Random(0x226226);
|
|
||||||
for (int i = 0; i < 200; i++)
|
|
||||||
{
|
{
|
||||||
var baseColour = NextColour(random);
|
TranslucencyKind.Opaque => GpuBlendMode.None,
|
||||||
var detail = new Vector4(NextColour(random), random.NextSingle());
|
TranslucencyKind.AlphaBlend => GpuBlendMode.StraightAlpha,
|
||||||
float opacity = random.NextSingle();
|
TranslucencyKind.Additive => GpuBlendMode.Additive,
|
||||||
var fog = NextColour(random);
|
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha,
|
||||||
float fogFactor = random.NextSingle();
|
TranslucencyKind.ClipMap when consumer == "EnvCell" =>
|
||||||
|
GpuBlendMode.PremultipliedAlpha,
|
||||||
|
TranslucencyKind.ClipMap => GpuBlendMode.None,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(expectedKind)),
|
||||||
|
}, state.Blend);
|
||||||
|
Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled));
|
||||||
|
if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap)
|
||||||
|
Assert.Equal("AP-240 immediate A2C", state.Placement);
|
||||||
|
else
|
||||||
|
Assert.Equal("source FIFO position", state.Placement);
|
||||||
|
|
||||||
Vector3 twoDrawOrder = RetailDetailTextureContract.ExpectedFogged(
|
Vector4 expectedSource = detailEnabled
|
||||||
baseColour, detail, opacity, fog, fogFactor);
|
? IndependentCombine(Base, Diffuse, Detail, 0.75f, 0.4f)
|
||||||
Vector3 retailOrder = Vector3.Lerp(
|
: new Vector4(
|
||||||
RetailDetailTextureContract.Expected(baseColour, detail, opacity),
|
new Vector3(Base.X, Base.Y, Base.Z) * Diffuse,
|
||||||
fog,
|
Base.W * 0.4f);
|
||||||
fogFactor);
|
Vector4 source = detailEnabled
|
||||||
|
? RetailDetailTextureContract.Combine(Base, Diffuse, Detail, 0.75f, 0.4f)
|
||||||
Assert.True(
|
: expectedSource;
|
||||||
MathF.Abs(twoDrawOrder.X - retailOrder.X) < 1e-6f,
|
AssertVector(expectedSource, source);
|
||||||
$"R: two-draw {twoDrawOrder.X} vs retail {retailOrder.X} (sample {i})");
|
AssertVector(IndependentComposite(source, Destination, family),
|
||||||
Assert.True(
|
RetailDetailTextureContract.Composite(source, Destination, family));
|
||||||
MathF.Abs(twoDrawOrder.Y - retailOrder.Y) < 1e-6f,
|
|
||||||
$"G: two-draw {twoDrawOrder.Y} vs retail {retailOrder.Y} (sample {i})");
|
|
||||||
Assert.True(
|
|
||||||
MathF.Abs(twoDrawOrder.Z - retailOrder.Z) < 1e-6f,
|
|
||||||
$"B: two-draw {twoDrawOrder.Z} vs retail {retailOrder.Z} (sample {i})");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Vector3 NextColour(Random random) =>
|
[Theory]
|
||||||
new(random.NextSingle(), random.NextSingle(), random.NextSingle());
|
[InlineData(SurfaceType.Translucent, TranslucencyKind.AlphaBlend, false)]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap,
|
||||||
|
TranslucencyKind.AlphaBlend, true)]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Alpha,
|
||||||
|
TranslucencyKind.AlphaBlend, false)]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.Additive,
|
||||||
|
TranslucencyKind.Additive, false)]
|
||||||
|
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha,
|
||||||
|
TranslucencyKind.InvAlpha, false)]
|
||||||
|
public void RawTranslucentOverride_PinsBlendAndClipTestPolicy(
|
||||||
|
SurfaceType type,
|
||||||
|
TranslucencyKind expectedKind,
|
||||||
|
bool rawClipWithoutAlphaFamily)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedKind, TranslucencyKindExtensions.FromSurfaceType(type));
|
||||||
|
byte mask = RetailAlphaMeshRouter.ConstructSubsetMask(
|
||||||
|
hasAlphaFamilyBit: (type & (SurfaceType.Alpha | SurfaceType.InvAlpha | SurfaceType.Additive)) != 0,
|
||||||
|
hasClipMapBit: (type & SurfaceType.Base1ClipMap) != 0,
|
||||||
|
hasTranslucentBit: true,
|
||||||
|
hasPositiveStippling: false);
|
||||||
|
Assert.Equal(rawClipWithoutAlphaFamily, (mask & RetailAlphaMeshRouter.MaskClipMap) != 0);
|
||||||
|
if ((type & SurfaceType.Translucent) != 0 && (type & SurfaceType.Base1ClipMap) != 0)
|
||||||
|
Assert.False(expectedKind == TranslucencyKind.ClipMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CombinePinsSquaredFinalAlphaAndExcludesBaseAlpha()
|
||||||
|
{
|
||||||
|
Vector4 actual = RetailDetailTextureContract.Combine(
|
||||||
|
Base, Diffuse, Detail, authoredOpacity: 0.75f, liveOpacity: 0.4f);
|
||||||
|
float a = 0.75f * 0.4f;
|
||||||
|
float w = a * Detail.W;
|
||||||
|
Vector3 expectedRgb = new(Detail.X, Detail.Y, Detail.Z);
|
||||||
|
expectedRgb = expectedRgb * w
|
||||||
|
+ new Vector3(Base.X, Base.Y, Base.Z) * Diffuse * (1f - w);
|
||||||
|
float expectedAlpha = a * Detail.W * Detail.W;
|
||||||
|
|
||||||
|
AssertVector(new Vector4(expectedRgb, expectedAlpha), actual);
|
||||||
|
Assert.NotEqual(a * Detail.W, actual.W);
|
||||||
|
Assert.NotEqual(Base.W * a * Detail.W * Detail.W, actual.W);
|
||||||
|
Vector3 oldTwoDrawRgb = new Vector3(Base.X, Base.Y, Base.Z) * Diffuse
|
||||||
|
* (new Vector3(Detail.X, Detail.Y, Detail.Z)
|
||||||
|
+ Vector3.One - new Vector3(Detail.W));
|
||||||
|
Assert.NotEqual(oldTwoDrawRgb.X, actual.X);
|
||||||
|
Assert.NotEqual(oldTwoDrawRgb.Y, actual.Y);
|
||||||
|
Assert.NotEqual(oldTwoDrawRgb.Z, actual.Z);
|
||||||
|
|
||||||
|
Vector4 changedBaseAlpha = new(Base.X, Base.Y, Base.Z, 0.97f);
|
||||||
|
AssertVector(actual, RetailDetailTextureContract.Combine(
|
||||||
|
changedBaseAlpha, Diffuse, Detail, 0.75f, 0.4f));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0)]
|
||||||
|
[InlineData(1)]
|
||||||
|
[InlineData(2)]
|
||||||
|
[InlineData(3)]
|
||||||
|
[InlineData(4)]
|
||||||
|
[InlineData(5)]
|
||||||
|
[InlineData(6)]
|
||||||
|
public void EveryFramebufferEquationUsesFinalX(int familyValue)
|
||||||
|
{
|
||||||
|
var family = (RetailDetailTextureContract.FramebufferFamily)familyValue;
|
||||||
|
Vector4 source = RetailDetailTextureContract.Combine(
|
||||||
|
Base, Diffuse, Detail, 0.75f, 0.4f);
|
||||||
|
float x = source.W;
|
||||||
|
Vector4 expected = family switch
|
||||||
|
{
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Opaque => source,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Alpha =>
|
||||||
|
source * x + Destination * (1f - x),
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive =>
|
||||||
|
source * x + Destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Additive =>
|
||||||
|
source + Destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.InverseAlpha =>
|
||||||
|
source * (1f - x) + Destination * x,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive =>
|
||||||
|
source * (1f - x) + Destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Clip =>
|
||||||
|
source + Destination * new Vector4(1f - x),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(family)),
|
||||||
|
};
|
||||||
|
AssertVector(expected, RetailDetailTextureContract.Composite(source, Destination, family));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(100f / 255f)]
|
||||||
|
[InlineData(200f / 255f)]
|
||||||
|
public void ClipBoundaryIsFinalAlphaGreaterEqual(float reference)
|
||||||
|
{
|
||||||
|
Assert.False(RetailDetailTextureContract.SurvivesClip(
|
||||||
|
MathF.BitDecrement(reference), reference));
|
||||||
|
Assert.True(RetailDetailTextureContract.SurvivesClip(reference, reference));
|
||||||
|
Assert.True(RetailDetailTextureContract.SurvivesClip(
|
||||||
|
MathF.BitIncrement(reference), reference));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FogRunsAfterCombineAndLeavesAlphaUntouched()
|
||||||
|
{
|
||||||
|
Vector4 source = RetailDetailTextureContract.Combine(
|
||||||
|
Base, Diffuse, Detail, 0.5f, 0.65f);
|
||||||
|
Vector3 fog = new(0.13f, 0.29f, 0.47f);
|
||||||
|
Vector4 fogged = RetailDetailTextureContract.ApplyFog(source, fog, 0.38f);
|
||||||
|
AssertVector(new Vector4(Vector3.Lerp(
|
||||||
|
new Vector3(source.X, source.Y, source.Z), fog, 0.38f), source.W), fogged);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample()
|
||||||
|
{
|
||||||
|
string ordinary = Shader("mesh_modern.frag");
|
||||||
|
string atmospheric = Shader("mesh_atmospheric.frag");
|
||||||
|
string shared = Shader("retail_detail_material.glsl");
|
||||||
|
|
||||||
|
Assert.Contains("#include \"retail_detail_material.glsl\"", ordinary, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("#include \"retail_detail_material.glsl\"", atmospheric, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("materialAlpha * detail.a * detail.a", shared, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("baseTexel.a", shared, StringComparison.Ordinal);
|
||||||
|
foreach (string shader in new[] { ordinary, atmospheric })
|
||||||
|
{
|
||||||
|
Assert.Contains("if (detailActive)", shader, StringComparison.Ordinal);
|
||||||
|
Assert.True(shader.IndexOf("if (detailActive)", StringComparison.Ordinal)
|
||||||
|
< shader.IndexOf("vec4 detail =", StringComparison.Ordinal));
|
||||||
|
Assert.Contains("vSurfaceOpacity * vOpacityMultiplier", shader, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
Assert.True(ordinary.IndexOf("if (uLightDebug == 3)", StringComparison.Ordinal)
|
||||||
|
< ordinary.IndexOf("vec4 detail =", StringComparison.Ordinal));
|
||||||
|
Assert.True(atmospheric.IndexOf("if (uLightDebug == 3)", StringComparison.Ordinal)
|
||||||
|
< atmospheric.IndexOf("vec4 detail =", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Shader(string file) => File.ReadAllText(Path.Combine(
|
||||||
|
RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders", file));
|
||||||
|
|
||||||
|
private static string RepositoryRoot() => Path.GetFullPath(Path.Combine(
|
||||||
|
AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
||||||
|
|
||||||
|
private static void AssertVector(Vector4 expected, Vector4 actual)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.X, actual.X, 6);
|
||||||
|
Assert.Equal(expected.Y, actual.Y, 6);
|
||||||
|
Assert.Equal(expected.Z, actual.Z, 6);
|
||||||
|
Assert.Equal(expected.W, actual.W, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] PhysicalDrawSequence(bool detailEnabled)
|
||||||
|
{
|
||||||
|
_ = detailEnabled;
|
||||||
|
return ["source-subset"];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DetailIsArmed(bool detailEnabled) => detailEnabled;
|
||||||
|
|
||||||
|
private static PipelineState ExpectedPipelineState(
|
||||||
|
string consumer,
|
||||||
|
TranslucencyKind kind,
|
||||||
|
bool paletted,
|
||||||
|
float reference)
|
||||||
|
{
|
||||||
|
_ = paletted;
|
||||||
|
GpuBlendMode blend = kind switch
|
||||||
|
{
|
||||||
|
TranslucencyKind.Opaque => GpuBlendMode.None,
|
||||||
|
TranslucencyKind.AlphaBlend => GpuBlendMode.StraightAlpha,
|
||||||
|
TranslucencyKind.Additive => GpuBlendMode.Additive,
|
||||||
|
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha,
|
||||||
|
TranslucencyKind.ClipMap when consumer == "EnvCell" =>
|
||||||
|
GpuBlendMode.PremultipliedAlpha,
|
||||||
|
TranslucencyKind.ClipMap => GpuBlendMode.None,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
|
||||||
|
};
|
||||||
|
return new PipelineState(
|
||||||
|
blend,
|
||||||
|
kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
|
||||||
|
AlphaTest: true,
|
||||||
|
reference,
|
||||||
|
consumer == "Building" && kind == TranslucencyKind.ClipMap
|
||||||
|
? "AP-240 immediate A2C"
|
||||||
|
: "source FIFO position");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4 IndependentCombine(
|
||||||
|
Vector4 baseTexel,
|
||||||
|
Vector3 diffuse,
|
||||||
|
Vector4 detail,
|
||||||
|
float authoredOpacity,
|
||||||
|
float liveOpacity)
|
||||||
|
{
|
||||||
|
float a = authoredOpacity * liveOpacity;
|
||||||
|
float w = a * detail.W;
|
||||||
|
Vector3 rgb = new(detail.X, detail.Y, detail.Z);
|
||||||
|
rgb = rgb * w
|
||||||
|
+ new Vector3(baseTexel.X, baseTexel.Y, baseTexel.Z)
|
||||||
|
* diffuse * (1f - w);
|
||||||
|
return new Vector4(rgb, a * detail.W * detail.W);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct PipelineState(
|
||||||
|
GpuBlendMode Blend,
|
||||||
|
bool DepthWrite,
|
||||||
|
bool AlphaTest,
|
||||||
|
float AlphaReference,
|
||||||
|
string Placement);
|
||||||
|
|
||||||
|
private static Vector4 IndependentComposite(
|
||||||
|
Vector4 source,
|
||||||
|
Vector4 destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily family)
|
||||||
|
{
|
||||||
|
float x = source.W;
|
||||||
|
return family switch
|
||||||
|
{
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Opaque => source,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Alpha =>
|
||||||
|
source * x + destination * (1f - x),
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive =>
|
||||||
|
source * x + destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Additive =>
|
||||||
|
source + destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.InverseAlpha =>
|
||||||
|
source * (1f - x) + destination * x,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive =>
|
||||||
|
source * (1f - x) + destination,
|
||||||
|
RetailDetailTextureContract.FramebufferFamily.Clip =>
|
||||||
|
source + destination * new Vector4(1f - x),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(family)),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ namespace AcDream.App.Tests.Rendering;
|
||||||
/// .TryCreateDetailTexture</c> (~TerrainAtlas.cs:488-556) must upload a FULL
|
/// .TryCreateDetailTexture</c> (~TerrainAtlas.cs:488-556) must upload a FULL
|
||||||
/// mip chain, and it must be sampled with the repeat/linear world sampler,
|
/// mip chain, and it must be sampled with the repeat/linear world sampler,
|
||||||
/// or the "attenuation is the sampler's linear mip chain" claim in
|
/// or the "attenuation is the sampler's linear mip chain" claim in
|
||||||
/// mesh_detail.vert's header comment and the VM1 commit is unverified.
|
/// the one-pass world material contract and the VM1 commit is unverified.
|
||||||
/// Drives the private method directly (reflection, same pattern as
|
/// Drives the private method directly (reflection, same pattern as
|
||||||
/// EnvCellRendererTests' private-method tests) against a synthetic
|
/// EnvCellRendererTests' private-method tests) against a synthetic
|
||||||
/// PFID_A8R8G8B8 RenderSurface, so no installed DAT is required — this lane
|
/// PFID_A8R8G8B8 RenderSurface, so no installed DAT is required — this lane
|
||||||
|
|
|
||||||
|
|
@ -285,19 +285,38 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PrepareThenDraw_ADetailCategoryCommandRecordsItsOwnSoloDraw()
|
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace()
|
||||||
{
|
{
|
||||||
using var fx = new DispatcherFixture();
|
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
|
||||||
using DrawScope draw = fx.BeginDraw();
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
|
||||||
OrderedDrawStream stream = StreamOf(
|
OrderedDrawStream stream = StreamOf(
|
||||||
MakeCommand(0),
|
MakeCommand(0),
|
||||||
MakeCommand(1, detailCategory: 1),
|
MakeCommand(1, detailCategory: 1),
|
||||||
MakeCommand(2));
|
MakeCommand(2, translucency: TranslucencyKind.ClipMap, detailCategory: 1),
|
||||||
|
MakeCommand(3));
|
||||||
|
|
||||||
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
|
||||||
|
|
||||||
Assert.Equal([(0, 1), (1, 1), (2, 1)], DecodeDrawRanges(fx.Device));
|
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
|
||||||
|
Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)],
|
||||||
|
runs.Select(run => (run.Start, run.Count)).ToList());
|
||||||
|
Assert.Equal((0u, 0f), (runs[0].Constants.TextureIndexA, runs[0].Constants.ParamA));
|
||||||
|
Assert.Equal((77u, 3.5f), (runs[1].Constants.TextureIndexA, runs[1].Constants.ParamA));
|
||||||
|
Assert.Equal((77u, 3.5f), (runs[2].Constants.TextureIndexA, runs[2].Constants.ParamA));
|
||||||
|
Assert.Equal((0u, 0f), (runs[3].Constants.TextureIndexA, runs[3].Constants.ParamA));
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
"wb-mesh-opaque-1x",
|
||||||
|
"wb-mesh-opaque-a2c-1x",
|
||||||
|
"wb-mesh-opaque-a2c-1x",
|
||||||
|
"wb-mesh-opaque-a2c-1x",
|
||||||
|
"wb-mesh-opaque-a2c-1x",
|
||||||
|
],
|
||||||
|
fx.Device.Calls.OfType<GpuRecordedPipelineBind>()
|
||||||
|
.Select(call => call.PipelineName)
|
||||||
|
.ToArray());
|
||||||
|
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -590,7 +609,7 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
private readonly WbMeshAdapter _meshAdapter;
|
private readonly WbMeshAdapter _meshAdapter;
|
||||||
private readonly TextureCache _textures;
|
private readonly TextureCache _textures;
|
||||||
|
|
||||||
public DispatcherFixture()
|
public DispatcherFixture(bool detailAvailable = false, bool detailEnabled = false)
|
||||||
{
|
{
|
||||||
Device = new RecordingGpuDevice();
|
Device = new RecordingGpuDevice();
|
||||||
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
FrameLifetime = new GpuDeviceFrameLifetime(Device);
|
||||||
|
|
@ -615,7 +634,12 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
_meshAdapter,
|
_meshAdapter,
|
||||||
entitySpawnAdapter,
|
entitySpawnAdapter,
|
||||||
new EntityClassificationCache(),
|
new EntityClassificationCache(),
|
||||||
new AcDream.Core.Rendering.TranslucencyFadeManager());
|
new AcDream.Core.Rendering.TranslucencyFadeManager(),
|
||||||
|
buildingDetail: detailAvailable
|
||||||
|
? new TerrainAtlas.RetailDetailTextureBinding(
|
||||||
|
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
|
||||||
|
: default,
|
||||||
|
buildingDetailEnabled: () => detailEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RecordingGpuDevice Device { get; }
|
public RecordingGpuDevice Device { get; }
|
||||||
|
|
|
||||||
|
|
@ -316,7 +316,7 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
|
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
Assert.Contains("161 active rows", register, StringComparison.Ordinal);
|
Assert.Contains("160 active rows", register, StringComparison.Ordinal);
|
||||||
int apSectionStart = register.IndexOf(
|
int apSectionStart = register.IndexOf(
|
||||||
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
|
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
|
||||||
int apSectionEnd = register.IndexOf(
|
int apSectionEnd = register.IndexOf(
|
||||||
|
|
@ -338,7 +338,7 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.Single(File.ReadLines(registerPath), static line =>
|
Assert.Single(File.ReadLines(registerPath), static line =>
|
||||||
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
|
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
161,
|
160,
|
||||||
File.ReadLines(registerPath).Count(static line =>
|
File.ReadLines(registerPath).Count(static line =>
|
||||||
line.StartsWith("| AP-", StringComparison.Ordinal)));
|
line.StartsWith("| AP-", StringComparison.Ordinal)));
|
||||||
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
|
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
|
||||||
|
|
@ -1120,9 +1120,9 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// S4-c2 final M3 production pin for <c>SubmitWalkAlphaInstance</c>:
|
/// S4-c2 final M3 production pin for <c>SubmitWalkAlphaInstance</c>:
|
||||||
/// building shell + detail ON executes the immediate base+detail draws at
|
/// building shell + detail ON executes one armed original-pipeline draw at
|
||||||
/// the submit mark and queues nothing; detail OFF queues, then drains the
|
/// the submit mark and queues nothing; detail OFF queues, then drains the
|
||||||
/// base draw without a detail pass; an ordinary batch remains deferred
|
/// neutral base draw; an ordinary batch remains deferred
|
||||||
/// even while the global detail option is on. Mutation check: hardcoding
|
/// even while the global detail option is on. Mutation check: hardcoding
|
||||||
/// <c>detailSurfaceActive=false</c> at the production submit makes the
|
/// <c>detailSurfaceActive=false</c> at the production submit makes the
|
||||||
/// first case's pending-count assertion fail with 1.</summary>
|
/// first case's pending-count assertion fail with 1.</summary>
|
||||||
|
|
@ -1145,9 +1145,11 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
call => call.PipelineName.Contains("alpha", StringComparison.Ordinal)
|
call => call.PipelineName.Contains("alpha", StringComparison.Ordinal)
|
||||||
&& !call.PipelineName.Contains("detail", StringComparison.Ordinal));
|
&& !call.PipelineName.Contains("detail", StringComparison.Ordinal));
|
||||||
Assert.Contains(
|
Assert.Single(on.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
Assert.DoesNotContain(on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
call => call.PipelineName.Contains("retail-detail-alpha", StringComparison.Ordinal));
|
call => call.PipelineName.Contains("detail", StringComparison.Ordinal));
|
||||||
|
Assert.Contains(on.Device.Calls.OfType<GpuRecordedPushConstants>(),
|
||||||
|
call => call.Constants.TextureIndexA == 99u && call.Constants.ParamA == 2f);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var off = new DispatcherFixture(
|
using (var off = new DispatcherFixture(
|
||||||
|
|
@ -1164,9 +1166,8 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
Assert.Equal(1, off.AlphaQueue!.PendingCount);
|
Assert.Equal(1, off.AlphaQueue!.PendingCount);
|
||||||
Assert.Empty(off.Device.Calls.OfType<GpuRecordedPipelineBind>());
|
Assert.Empty(off.Device.Calls.OfType<GpuRecordedPipelineBind>());
|
||||||
off.AlphaQueue.EndFrame();
|
off.AlphaQueue.EndFrame();
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(off.Device.Calls.OfType<GpuRecordedPushConstants>(),
|
||||||
off.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
call => call.Constants.TextureIndexA == 99u || call.Constants.ParamA != 0f);
|
||||||
call => call.PipelineName.Contains("retail-detail", StringComparison.Ordinal));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var ordinary = new DispatcherFixture(
|
using (var ordinary = new DispatcherFixture(
|
||||||
|
|
@ -1212,12 +1213,41 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
defer.Invoke(fx.Dispatcher, [Matrix4x4.Identity]);
|
defer.Invoke(fx.Dispatcher, [Matrix4x4.Identity]);
|
||||||
|
|
||||||
Assert.Equal(0, fx.AlphaQueue!.PendingCount);
|
Assert.Equal(0, fx.AlphaQueue!.PendingCount);
|
||||||
Assert.Contains(
|
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
Assert.DoesNotContain(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
call => call.PipelineName.Contains("retail-detail-alpha", StringComparison.Ordinal));
|
call => call.PipelineName.Contains("detail", StringComparison.Ordinal));
|
||||||
|
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPushConstants>(),
|
||||||
|
call => call.Constants.TextureIndexA == 99u && call.Constants.ParamA == 2f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(uint detailCategory) =>
|
[Theory]
|
||||||
|
[InlineData(TranslucencyKind.AlphaBlend, "wb-mesh-alpha-1x")]
|
||||||
|
[InlineData(TranslucencyKind.Additive, "wb-mesh-additive-1x")]
|
||||||
|
[InlineData(TranslucencyKind.InvAlpha, "wb-mesh-inverse-1x")]
|
||||||
|
public void ImmediateBuildingDetail_RetainsOriginalFramebufferFamily(
|
||||||
|
TranslucencyKind kind,
|
||||||
|
string expectedPipeline)
|
||||||
|
{
|
||||||
|
using var fx = new DispatcherFixture(
|
||||||
|
withAlphaQueue: true,
|
||||||
|
detailAvailable: true,
|
||||||
|
detailEnabled: true);
|
||||||
|
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
|
||||||
|
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
|
||||||
|
detailCategory: 1u,
|
||||||
|
translucency: kind);
|
||||||
|
|
||||||
|
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
|
||||||
|
|
||||||
|
Assert.Equal(0, fx.AlphaQueue!.PendingCount);
|
||||||
|
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
|
||||||
|
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
|
||||||
|
call => call.PipelineName == expectedPipeline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(
|
||||||
|
uint detailCategory,
|
||||||
|
TranslucencyKind translucency = TranslucencyKind.AlphaBlend) =>
|
||||||
new(
|
new(
|
||||||
new GroupKey(
|
new GroupKey(
|
||||||
0,
|
0,
|
||||||
|
|
@ -1225,7 +1255,7 @@ public sealed class WalkStaticStreamPopulatorTests
|
||||||
3,
|
3,
|
||||||
new GpuTextureSlot(1),
|
new GpuTextureSlot(1),
|
||||||
0,
|
0,
|
||||||
TranslucencyKind.AlphaBlend,
|
translucency,
|
||||||
FoliageFlags: 0),
|
FoliageFlags: 0),
|
||||||
Matrix4x4.Identity,
|
Matrix4x4.Identity,
|
||||||
ClipSlot: 0,
|
ClipSlot: 0,
|
||||||
|
|
|
||||||
|
|
@ -101,16 +101,16 @@ public class EnvCellRendererTests
|
||||||
Vector4.Zero,
|
Vector4.Zero,
|
||||||
sampleCount: 1));
|
sampleCount: 1));
|
||||||
|
|
||||||
EnvCellRenderer.BindEnvironmentDetailCategory(pass, frame);
|
using EnvCellRenderer renderer = CreateRenderer();
|
||||||
|
renderer.BindEnvironmentDetailCategory(pass, frame, instanceCount: 3);
|
||||||
|
|
||||||
GpuRecordedStorageBind storageBind = Assert.Single(
|
GpuRecordedStorageBind storageBind = Assert.Single(
|
||||||
device.Calls.OfType<GpuRecordedStorageBind>());
|
device.Calls.OfType<GpuRecordedStorageBind>());
|
||||||
Assert.Equal(GpuBindingModel.StorageInstanceDetailCategory, storageBind.Binding);
|
Assert.Equal(GpuBindingModel.StorageInstanceDetailCategory, storageBind.Binding);
|
||||||
Assert.Equal((uint)sizeof(uint), storageBind.SizeBytes);
|
Assert.Equal((uint)(3 * sizeof(uint)), storageBind.SizeBytes);
|
||||||
Assert.Equal(
|
ReadOnlySpan<uint> categories = MemoryMarshal.Cast<byte, uint>(
|
||||||
1u,
|
device.RingBytes.Slice((int)storageBind.OffsetBytes, (int)storageBind.SizeBytes));
|
||||||
MemoryMarshal.Read<uint>(
|
Assert.Equal([1u, 1u, 1u], categories.ToArray());
|
||||||
device.RingBytes.Slice((int)storageBind.OffsetBytes, sizeof(uint))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -118,17 +118,14 @@ public class EnvCellRendererTests
|
||||||
/// StorageInstances/StorageBatches/StorageClipSlots/StorageGlobalLights/
|
/// StorageInstances/StorageBatches/StorageClipSlots/StorageGlobalLights/
|
||||||
/// StorageInstanceLightSets every frame but never
|
/// StorageInstanceLightSets every frame but never
|
||||||
/// GpuBindingModel.StorageInstanceAlpha (binding 7) — the SSBO both
|
/// GpuBindingModel.StorageInstanceAlpha (binding 7) — the SSBO both
|
||||||
/// mesh_modern.vert (interior shells) and mesh_detail.vert (interior
|
/// both world vertex families read as instanceAlpha[instanceIndex].
|
||||||
/// detail replay) read as instanceAlpha[instanceIndex]. Predates VM1
|
/// Predates VM1 (6c79d35c has the same omission); an unfixed gap here
|
||||||
/// (6c79d35c has the same omission); with mesh_detail.vert now reading
|
/// would have made the interior detail material read whatever section a DIFFERENT renderer (or a
|
||||||
/// that binding too, an unfixed gap here would have made the interior
|
|
||||||
/// detail overlay read whatever section a DIFFERENT renderer (or a
|
|
||||||
/// stale ring slot) last left in binding 7, indexed by these cells'
|
/// stale ring slot) last left in binding 7, indexed by these cells'
|
||||||
/// instance ids. This pins that SubmitRhi binds its own constant-1.0f
|
/// instance ids. This pins that SubmitRhi binds its own constant-1.0f
|
||||||
/// section, sized to the live instance count, before it records any
|
/// section, sized to the live instance count, before it records any
|
||||||
/// draw in the pass — one bind serves every subsequent draw in that
|
/// draw in the pass — one bind serves every subsequent draw in that
|
||||||
/// pass, mesh_modern's shell pipeline and (when enabled) mesh_detail's
|
/// pass and every shell pipeline.
|
||||||
/// replay alike.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(WbRenderPass.Opaque)]
|
[InlineData(WbRenderPass.Opaque)]
|
||||||
|
|
@ -242,7 +239,7 @@ public class EnvCellRendererTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RetailDetailPipelinesPreserveOpaqueAndTransparentDepthWriteContracts()
|
public void DetailUsesOnlyTheOriginalFourShellPipelines()
|
||||||
{
|
{
|
||||||
using var device = new RecordingGpuDevice();
|
using var device = new RecordingGpuDevice();
|
||||||
using var meshManager = CreateMeshManager(device);
|
using var meshManager = CreateMeshManager(device);
|
||||||
|
|
@ -253,23 +250,9 @@ public class EnvCellRendererTests
|
||||||
meshManager,
|
meshManager,
|
||||||
new WbFrustum());
|
new WbFrustum());
|
||||||
|
|
||||||
GpuPipelineDescription opaqueDetail = Assert.Single(
|
Assert.Equal(4, device.CreatedPipelines.Count);
|
||||||
device.CreatedPipelines,
|
Assert.DoesNotContain(device.CreatedPipelines,
|
||||||
pipeline => pipeline.Description.Name == "envcell-retail-detail")
|
pipeline => pipeline.Description.Name.Contains("detail", StringComparison.Ordinal));
|
||||||
.Description;
|
|
||||||
GpuPipelineDescription transparentDetail = Assert.Single(
|
|
||||||
device.CreatedPipelines,
|
|
||||||
pipeline => pipeline.Description.Name == "envcell-retail-detail-alpha")
|
|
||||||
.Description;
|
|
||||||
|
|
||||||
Assert.Equal(GpuBlendMode.RetailDetail, opaqueDetail.Blend);
|
|
||||||
Assert.True(opaqueDetail.Depth.Write);
|
|
||||||
Assert.Equal(GpuCompareOp.Equal, opaqueDetail.Depth.Compare);
|
|
||||||
Assert.False(opaqueDetail.AlphaToCoverage);
|
|
||||||
Assert.Equal(GpuBlendMode.RetailDetail, transparentDetail.Blend);
|
|
||||||
Assert.False(transparentDetail.Depth.Write);
|
|
||||||
Assert.Equal(GpuCompareOp.LessOrEqual, transparentDetail.Depth.Compare);
|
|
||||||
Assert.False(transparentDetail.AlphaToCoverage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ namespace AcDream.App.Tests.Rendering.Wb;
|
||||||
///
|
///
|
||||||
/// Layout under test (std430, 16 bytes total):
|
/// Layout under test (std430, 16 bytes total):
|
||||||
/// offset 0 : uint TextureTableIndex
|
/// offset 0 : uint TextureTableIndex
|
||||||
/// offset 4 : uint Reserved (pad)
|
/// offset 4 : float SurfaceOpacity
|
||||||
/// offset 8 : uint TextureIndex (layer within the texture array)
|
/// offset 8 : uint TextureIndex (layer within the texture array)
|
||||||
/// offset 12 : uint Flags
|
/// offset 12 : uint Flags
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -36,7 +36,7 @@ public class ModernBatchDataLayoutTests
|
||||||
public void FieldOffsets_MatchStd430Layout()
|
public void FieldOffsets_MatchStd430Layout()
|
||||||
{
|
{
|
||||||
Assert.Equal(0, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.TextureTableIndex)));
|
Assert.Equal(0, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.TextureTableIndex)));
|
||||||
Assert.Equal(4, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.Reserved)));
|
Assert.Equal(4, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.SurfaceOpacity)));
|
||||||
Assert.Equal(8, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.TextureIndex)));
|
Assert.Equal(8, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.TextureIndex)));
|
||||||
Assert.Equal(12, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.Flags)));
|
Assert.Equal(12, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.Flags)));
|
||||||
}
|
}
|
||||||
|
|
@ -47,11 +47,12 @@ public class ModernBatchDataLayoutTests
|
||||||
var data = new ModernBatchData
|
var data = new ModernBatchData
|
||||||
{
|
{
|
||||||
TextureTableIndex = 7u,
|
TextureTableIndex = 7u,
|
||||||
|
SurfaceOpacity = 0.25f,
|
||||||
TextureIndex = 3u,
|
TextureIndex = 3u,
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.Equal(7u, data.TextureTableIndex);
|
Assert.Equal(7u, data.TextureTableIndex);
|
||||||
Assert.Equal(0u, data.Reserved);
|
Assert.Equal(0.25f, data.SurfaceOpacity);
|
||||||
Assert.Equal(3u, data.TextureIndex);
|
Assert.Equal(3u, data.TextureIndex);
|
||||||
Assert.Equal(0u, data.Flags);
|
Assert.Equal(0u, data.Flags);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ public sealed class MeshExtractorSolidFaceExtractionTests
|
||||||
var dats = new FakeMeshExtractorDats();
|
var dats = new FakeMeshExtractorDats();
|
||||||
RegisterTexturedQuad(
|
RegisterTexturedQuad(
|
||||||
dats,
|
dats,
|
||||||
SurfaceType.Base1Image,
|
SurfaceType.Base1Image | SurfaceType.Translucent,
|
||||||
PixelFormat.PFID_DXT1,
|
PixelFormat.PFID_DXT1,
|
||||||
new byte[8],
|
new byte[8],
|
||||||
translucency: 0.25f);
|
translucency: 0.25f);
|
||||||
|
|
@ -219,7 +219,40 @@ public sealed class MeshExtractorSolidFaceExtractionTests
|
||||||
KeyValuePair<(int Width, int Height, TextureFormat Format), List<TextureBatchData>> group =
|
KeyValuePair<(int Width, int Height, TextureFormat Format), List<TextureBatchData>> group =
|
||||||
Assert.Single(mesh.TextureBatches);
|
Assert.Single(mesh.TextureBatches);
|
||||||
Assert.Equal(TextureFormat.RGBA8, group.Key.Format);
|
Assert.Equal(TextureFormat.RGBA8, group.Key.Format);
|
||||||
Assert.Equal(4 * 4 * 4, Assert.Single(group.Value).TextureData.Length);
|
TextureBatchData batch = Assert.Single(group.Value);
|
||||||
|
Assert.Equal(4 * 4 * 4, batch.TextureData.Length);
|
||||||
|
Assert.Equal(0.75f, batch.SurfaceOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0f, 1f)]
|
||||||
|
[InlineData(0.25f, 0.75f)]
|
||||||
|
[InlineData(0.5f, 0.5f)]
|
||||||
|
[InlineData(0.75f, 0.25f)]
|
||||||
|
[InlineData(1f, 0f)]
|
||||||
|
public void PrepareCellStructMeshData_CarriesAuthoredSurfaceOpacity(
|
||||||
|
float translucency,
|
||||||
|
float expectedOpacity)
|
||||||
|
{
|
||||||
|
var dats = new FakeMeshExtractorDats();
|
||||||
|
RegisterCellTexturedSurface(
|
||||||
|
dats,
|
||||||
|
SurfaceType.Base1Image | SurfaceType.Translucent,
|
||||||
|
translucency);
|
||||||
|
var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null);
|
||||||
|
|
||||||
|
ObjectMeshData mesh = Assert.IsType<ObjectMeshData>(
|
||||||
|
extractor.PrepareCellStructMeshData(
|
||||||
|
id: 1,
|
||||||
|
BuildQuadCellStruct(RetailCullMode.Landblock),
|
||||||
|
surfaceOverrides: [2],
|
||||||
|
Matrix4x4.Identity,
|
||||||
|
CancellationToken.None));
|
||||||
|
|
||||||
|
TextureBatchData batch = Assert.Single(Assert.Single(mesh.TextureBatches).Value);
|
||||||
|
Assert.Equal(
|
||||||
|
BitConverter.SingleToInt32Bits(expectedOpacity),
|
||||||
|
BitConverter.SingleToInt32Bits(batch.SurfaceOpacity));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -232,12 +265,16 @@ public sealed class MeshExtractorSolidFaceExtractionTests
|
||||||
/// not change what these tests actually pin (verified by hand against
|
/// not change what these tests actually pin (verified by hand against
|
||||||
/// CellStructSideCandidates.TriangleFanIndices before this change).
|
/// CellStructSideCandidates.TriangleFanIndices before this change).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void RegisterCellTexturedSurface(FakeMeshExtractorDats dats)
|
private static void RegisterCellTexturedSurface(
|
||||||
|
FakeMeshExtractorDats dats,
|
||||||
|
SurfaceType type = SurfaceType.Base1Image,
|
||||||
|
float translucency = 0f)
|
||||||
{
|
{
|
||||||
dats.Register(TexturedSurfaceId, new Surface
|
dats.Register(TexturedSurfaceId, new Surface
|
||||||
{
|
{
|
||||||
Type = SurfaceType.Base1Image,
|
Type = type,
|
||||||
OrigTextureId = SurfaceTextureId,
|
OrigTextureId = SurfaceTextureId,
|
||||||
|
Translucency = translucency,
|
||||||
});
|
});
|
||||||
dats.Register(SurfaceTextureId, new SurfaceTexture
|
dats.Register(SurfaceTextureId, new SurfaceTexture
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,9 @@ public static class ObjectMeshDataEquality {
|
||||||
Assert.True(expected.SourceSurfaceIndex == actual.SourceSurfaceIndex, $"{path}.SourceSurfaceIndex: expected {expected.SourceSurfaceIndex}, got {actual.SourceSurfaceIndex}");
|
Assert.True(expected.SourceSurfaceIndex == actual.SourceSurfaceIndex, $"{path}.SourceSurfaceIndex: expected {expected.SourceSurfaceIndex}, got {actual.SourceSurfaceIndex}");
|
||||||
Assert.True(expected.RetailSurfaceMask == actual.RetailSurfaceMask, $"{path}.RetailSurfaceMask: expected {expected.RetailSurfaceMask}, got {actual.RetailSurfaceMask}");
|
Assert.True(expected.RetailSurfaceMask == actual.RetailSurfaceMask, $"{path}.RetailSurfaceMask: expected {expected.RetailSurfaceMask}, got {actual.RetailSurfaceMask}");
|
||||||
Assert.True(expected.RawSurfaceType == actual.RawSurfaceType, $"{path}.RawSurfaceType: expected {expected.RawSurfaceType}, got {actual.RawSurfaceType}");
|
Assert.True(expected.RawSurfaceType == actual.RawSurfaceType, $"{path}.RawSurfaceType: expected {expected.RawSurfaceType}, got {actual.RawSurfaceType}");
|
||||||
|
Assert.True(BitConverter.SingleToInt32Bits(expected.SurfaceOpacity)
|
||||||
|
== BitConverter.SingleToInt32Bits(actual.SurfaceOpacity),
|
||||||
|
$"{path}.SurfaceOpacity: expected {expected.SurfaceOpacity:R}, got {actual.SurfaceOpacity:R}");
|
||||||
Assert.True(expected.IsCellShell == actual.IsCellShell, $"{path}.IsCellShell: expected {expected.IsCellShell}, got {actual.IsCellShell}");
|
Assert.True(expected.IsCellShell == actual.IsCellShell, $"{path}.IsCellShell: expected {expected.IsCellShell}, got {actual.IsCellShell}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,7 @@ public class ObjectMeshDataSerializerTests {
|
||||||
SourceSurfaceIndex = 7,
|
SourceSurfaceIndex = 7,
|
||||||
RetailSurfaceMask = 0b0000_1101,
|
RetailSurfaceMask = 0b0000_1101,
|
||||||
RawSurfaceType = 0x11u,
|
RawSurfaceType = 0x11u,
|
||||||
|
SurfaceOpacity = 0.25f,
|
||||||
IsCellShell = true,
|
IsCellShell = true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -265,6 +266,30 @@ public class ObjectMeshDataSerializerTests {
|
||||||
ObjectMeshDataEquality.AssertEqual(original, readBack);
|
ObjectMeshDataEquality.AssertEqual(original, readBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(1f)]
|
||||||
|
[InlineData(0.75f)]
|
||||||
|
[InlineData(0.5f)]
|
||||||
|
[InlineData(0.25f)]
|
||||||
|
[InlineData(0f)]
|
||||||
|
public void SurfaceOpacity_RoundTripsBitExactlyAndDeterministically(float opacity) {
|
||||||
|
ObjectMeshData data = WithCellShellSubset();
|
||||||
|
TextureBatchData batch = Assert.Single(Assert.Single(data.TextureBatches).Value);
|
||||||
|
batch.SurfaceOpacity = opacity;
|
||||||
|
|
||||||
|
using var first = new MemoryStream();
|
||||||
|
ObjectMeshDataSerializer.Write(data, first);
|
||||||
|
using var second = new MemoryStream();
|
||||||
|
ObjectMeshDataSerializer.Write(data, second);
|
||||||
|
|
||||||
|
Assert.Equal(first.ToArray(), second.ToArray());
|
||||||
|
TextureBatchData read = Assert.Single(Assert.Single(
|
||||||
|
ObjectMeshDataSerializer.Read(first.ToArray()).TextureBatches).Value);
|
||||||
|
Assert.Equal(
|
||||||
|
BitConverter.SingleToInt32Bits(opacity),
|
||||||
|
BitConverter.SingleToInt32Bits(read.SurfaceOpacity));
|
||||||
|
}
|
||||||
|
|
||||||
// ---- determinism -----------------------------------------------------
|
// ---- determinism -----------------------------------------------------
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,13 @@ public class PakFormatTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// OH2/S1 contract §8.3 point 5: the recipe 7 -> 8 bump (exact
|
/// S5-c4: recipe 9 carries authored surface opacity in every prepared
|
||||||
/// CellStruct surface-index subset construction) is a serialized
|
/// texture batch. It remains a payload recipe change, not pak framing.
|
||||||
/// PAYLOAD/recipe change, not a container framing change — this pin
|
|
||||||
/// makes that decision explicit and test-visible rather than implicit.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatVersion_StaysAtTwo_AcrossTheOH2RecipeBump() {
|
public void FormatVersion_StaysAtTwo_AcrossTheS5C4RecipeBump() {
|
||||||
Assert.Equal(2u, PakFormat.CurrentFormatVersion);
|
Assert.Equal(2u, PakFormat.CurrentFormatVersion);
|
||||||
Assert.Equal(8u, PakFormat.CurrentBakeToolVersion);
|
Assert.Equal(9u, PakFormat.CurrentBakeToolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -136,12 +136,11 @@ public sealed class PreparedAssetSourceTests : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// OH2/S1 contract §10.5: an OLDER-recipe (7) package must be rejected
|
/// S5-c4: an older recipe-8 package must be rejected by a recipe-9
|
||||||
/// by a CURRENT-recipe (8) consumer through the catalog-identity check
|
/// consumer through the catalog-identity check
|
||||||
/// alone, BEFORE any TextureBatchData payload field is ever
|
/// alone, BEFORE any TextureBatchData payload field is ever
|
||||||
/// deserialized -- so an older pak can never be silently misread as
|
/// deserialized -- so an older pak can never be silently misread as
|
||||||
/// carrying the new SourceSurfaceIndex/RetailSurfaceMask/RawSurfaceType/
|
/// carrying authored surface opacity.
|
||||||
/// IsCellShell fields.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Constructor_RejectsOlderRecipePackageBeforePayloadDecode()
|
public void Constructor_RejectsOlderRecipePackageBeforePayloadDecode()
|
||||||
|
|
@ -161,7 +160,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
|
||||||
fs.Position = 36;
|
fs.Position = 36;
|
||||||
Span<byte> buf = stackalloc byte[4];
|
Span<byte> buf = stackalloc byte[4];
|
||||||
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
|
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
|
||||||
buf, PakFormat.CurrentBakeToolVersion - 1);
|
buf, 8u);
|
||||||
fs.Write(buf);
|
fs.Write(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,6 +168,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
|
||||||
() => new PakPreparedAssetSource(path, Identity));
|
() => new PakPreparedAssetSource(path, Identity));
|
||||||
Assert.Contains("does not match the installed DAT set", error.Message);
|
Assert.Contains("does not match the installed DAT set", error.Message);
|
||||||
Assert.Contains("Re-bake", error.Message);
|
Assert.Contains("Re-bake", error.Message);
|
||||||
|
Assert.Contains("bake tool 8 != 9", error.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
||||||
// Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances)
|
// Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances)
|
||||||
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
||||||
{
|
{
|
||||||
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque),
|
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.75f),
|
||||||
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend),
|
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, SurfaceOpacity: 0.25f),
|
||||||
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque),
|
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.5f),
|
||||||
};
|
};
|
||||||
|
|
||||||
var indirect = new DrawElementsIndirectCommand[16];
|
var indirect = new DrawElementsIndirectCommand[16];
|
||||||
|
|
@ -60,6 +60,9 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
||||||
Assert.Equal(0xAAu, batch[0].TextureIndex);
|
Assert.Equal(0xAAu, batch[0].TextureIndex);
|
||||||
Assert.Equal(0xCCu, batch[1].TextureIndex);
|
Assert.Equal(0xCCu, batch[1].TextureIndex);
|
||||||
Assert.Equal(0xBBu, batch[2].TextureIndex);
|
Assert.Equal(0xBBu, batch[2].TextureIndex);
|
||||||
|
Assert.Equal(0.75f, batch[0].SurfaceOpacity);
|
||||||
|
Assert.Equal(0.5f, batch[1].SurfaceOpacity);
|
||||||
|
Assert.Equal(0.25f, batch[2].SurfaceOpacity);
|
||||||
Assert.Equal(CullMode.CounterClockwise, cull[0]);
|
Assert.Equal(CullMode.CounterClockwise, cull[0]);
|
||||||
Assert.Equal(CullMode.CounterClockwise, cull[1]);
|
Assert.Equal(CullMode.CounterClockwise, cull[1]);
|
||||||
Assert.Equal(CullMode.CounterClockwise, cull[2]);
|
Assert.Equal(CullMode.CounterClockwise, cull[2]);
|
||||||
|
|
@ -184,8 +187,8 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
||||||
];
|
];
|
||||||
|
|
||||||
// Command 1 is mixed: instance 2 is ordinary and instance 3 is a
|
// Command 1 is mixed: instance 2 is ordinary and instance 3 is a
|
||||||
// building. It must be submitted once, then mesh_detail filters the
|
// building. It must form a solo one-pass detail run. Commands 0 and 2
|
||||||
// ordinary instance. Commands 0 and 2 must never reach the detail pipe.
|
// remain neutral ordinary runs.
|
||||||
uint[] mixedCategories = [0u, 0u, 0u, 1u, 0u];
|
uint[] mixedCategories = [0u, 0u, 0u, 1u, 0u];
|
||||||
Assert.True(WbDrawDispatcher.TryGetNextDetailCommandRun(
|
Assert.True(WbDrawDispatcher.TryGetNextDetailCommandRun(
|
||||||
commands,
|
commands,
|
||||||
|
|
@ -238,17 +241,13 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
||||||
// without copying. The cast is only safe if the structs have identical
|
// without copying. The cast is only safe if the structs have identical
|
||||||
// layout (size, field offsets).
|
// layout (size, field offsets).
|
||||||
//
|
//
|
||||||
// Campaign V slice V2 (2026-07-27): TextureHandle (ulong, an
|
// S5-c4 consumes the former pad word as authored SurfaceOpacity. The
|
||||||
// ARB_bindless_texture handle) became TextureIndex (uint) plus an
|
// struct stays 16 bytes and TextureLayer/Flags keep their offsets
|
||||||
// explicit Reserved pad word — a slot into the GL-only binding=9 handle
|
// (8/12), matching GpuBindingModel.GpuBatchDataStrideBytes and both
|
||||||
// table instead of the raw handle (that binding, GpuBindingModel's
|
// world shader families.
|
||||||
// former StorageTextureTable, was deleted at Campaign V slice V11).
|
|
||||||
// The struct stays 16 bytes and TextureLayer/Flags keep their offsets
|
|
||||||
// (8/12), matching GpuBindingModel.GpuBatchDataStrideBytes and every
|
|
||||||
// existing CPU writer, so both structs only need 4-byte packing now.
|
|
||||||
Assert.Equal(16, System.Runtime.CompilerServices.Unsafe.SizeOf<WbDrawDispatcher.BatchDataPublic>());
|
Assert.Equal(16, System.Runtime.CompilerServices.Unsafe.SizeOf<WbDrawDispatcher.BatchDataPublic>());
|
||||||
Assert.Equal(0, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.TextureIndex)));
|
Assert.Equal(0, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.TextureIndex)));
|
||||||
Assert.Equal(4, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.Reserved)));
|
Assert.Equal(4, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.SurfaceOpacity)));
|
||||||
Assert.Equal(8, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.TextureLayer)));
|
Assert.Equal(8, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.TextureLayer)));
|
||||||
Assert.Equal(12, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.Flags)));
|
Assert.Equal(12, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.Flags)));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,4 +53,28 @@ public sealed class ContentMigrationCatalogTests
|
||||||
Assert.Contains("DrawingBSP", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("DrawingBSP", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Contains("CellStruct", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
Assert.Contains("CellStruct", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RecipeEightToNineRequiresOneExplicitFullRebuild()
|
||||||
|
{
|
||||||
|
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(8, 9);
|
||||||
|
|
||||||
|
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
|
||||||
|
Assert.Equal(8u, plan.FromRecipeVersion);
|
||||||
|
Assert.Equal(9u, plan.TargetRecipeVersion);
|
||||||
|
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Empty(plan.EffectiveDatIds);
|
||||||
|
Assert.Empty(plan.EffectiveLandblocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnyOlderRecipeToNineCollapsesToOneFullRebuild()
|
||||||
|
{
|
||||||
|
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(1, 9);
|
||||||
|
|
||||||
|
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
|
||||||
|
Assert.Equal(9u, plan.TargetRecipeVersion);
|
||||||
|
Assert.Contains("CellStruct", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue