Opus dual-lens review of05970306+388457a7(APPROVE WITH FIXES). Four items, all landed: 1. FOG (behavioural). Retail's D3D fixed-function fog stage runs AFTER the texture-stage pipeline, so the detail contribution must be fogged, not just the base. mesh_modern.frag already fogs the base colour (applyFog(rgb, vWorldPos)) before mesh_detail's replay draws over it; mesh_detail.frag previously emitted raw detail.rgb, understating fog by f*a*(fog-detail). Fix: mesh_detail.vert now outputs vWorldPos (mirroring mesh_modern.vert); mesh_detail.frag declares the identical SceneLighting UBO and applyFog function (copied verbatim, same binding/std140/math) and fogs detail.rgb before emitting it. This collapses algebraically to retail's fog-after-combine order: (1-a)*mix(base,fog,f) + a*mix(detail,fog,f) = mix(lerp(base,detail,a),fog,f) RetailDetailTextureContract gains ExpectedFogged(base,detail,opacity,fog, fogFactor); RetailDetailTextureContractTests pins the identity across 200 random samples within 1e-6. 2. EnvCellRenderer.Rhi.cs's DrawEnvCell-category comment still said "apply the 10-50 m positive-view-depth fade" — a stale claim from before VM1 removed the fade. Replaced with the mip-chain attenuation statement that mesh_detail.vert's header comment already carries. 3. Added the test the VM1 contract required but never had: TerrainAtlas .TryCreateDetailTexture uploads a full mip chain (MipLevelCount == RhiWorldTextureArray.MipLevelsFor(w,h), GenerateMipChain called) and registers with the repeat/linear world sampler, not single-level or clamped. Drives the private method directly (reflection) against a synthetic PFID_A8R8G8B8 RenderSurface through a minimal in-memory IDatReaderWriter fake, so the lane stays hermetic (no installed DAT). 4. #226 pseudocode note: noted that retail's stage-1 OUTPUT alpha (MODULATE(TEXTURE, CURRENT), 0x0059c549) — the framebuffer blend weight a delayed-alpha subset composites with — is not modelled; acdream instead draws a second pass weighted by detail.a*diffuseAlpha. Identical for opaque subsets, a bounded difference on translucent building/EnvCell subsets already covered by the existing AP-34 shared-alpha-queue divergence row. Also qualified the tmpmaterial.Diffuse.a = 1f (0x0059cb99) citation to name its exact branch (burnedInStaticLights < 0 && *(render_device+0x7e4) == 0); the other branch leaves diffuse FromVertex, but the opaque->1 / fading->opacity mapping still holds either way. Nit also folded in: EnvCellRendererTests' new SubmitRhi instance-alpha test is now a [Theory] over WbRenderPass.Opaque and .Transparent, pinning the bind-before-first-draw invariant on both passes. Regenerated mesh_detail's committed SPIR-V and the shader manifest (tools/compile-shaders.ps1); no other shader pair changed. Verified: dotnet build AcDream.slnx -c Release (0 warnings, 0 errors); dotnet test on AcDream.App.Tests (Release, hermetic lanes) green, including the shader manifest tests explicitly; AcDream.Core.Tests unaffected/green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
148 lines
6.2 KiB
C#
148 lines
6.2 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class RetailDetailTextureContractTests
|
|
{
|
|
private static readonly TerrainAtlas.RetailDetailTextureBinding Available = new(
|
|
new GpuTextureSlot(7),
|
|
Tiling: 4f,
|
|
SurfaceTextureId: 0x05001787,
|
|
RenderSurfaceId: 0x06006D58,
|
|
Width: 256,
|
|
Height: 256);
|
|
|
|
[Fact]
|
|
public void ExistingBuildingDetailSettingIsTheLivePassGate()
|
|
{
|
|
Assert.False(RetailDetailTextureContract.ShouldRender(false, Available));
|
|
Assert.True(RetailDetailTextureContract.ShouldRender(true, Available));
|
|
Assert.False(RetailDetailTextureContract.ShouldRender(true, default));
|
|
}
|
|
|
|
[Fact]
|
|
public void OpaqueDetailUsesDepthEqualityWhileTransparentDetailDoesNot()
|
|
{
|
|
Assert.Equal(
|
|
GpuCompareOp.Equal,
|
|
RetailDetailTextureContract.DetailDepthCompare(transparent: false));
|
|
Assert.Equal(
|
|
GpuCompareOp.LessOrEqual,
|
|
RetailDetailTextureContract.DetailDepthCompare(transparent: true));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.1f, 0.2f, 0.3f)]
|
|
[InlineData(0.0f, 0.0f, 0.0f)]
|
|
[InlineData(1.0f, 1.0f, 1.0f)]
|
|
[InlineData(0.5f, 0.9f, 0.05f)]
|
|
public void ZeroDetailAlphaIsExactNoOp(float r, float g, float b)
|
|
{
|
|
var baseColour = new Vector3(r, g, b);
|
|
var transparentDetail = new Vector4(0.9f, 0.1f, 0.7f, 0f);
|
|
|
|
Assert.Equal(
|
|
baseColour,
|
|
RetailDetailTextureContract.Expected(baseColour, transparentDetail, opacity: 1f));
|
|
Assert.True(RetailDetailTextureContract.IsNeutral(transparentDetail, opacity: 1f));
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroOpacityIsExactNoOp()
|
|
{
|
|
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);
|
|
var detail = new Vector4(NextColour(random), random.NextSingle());
|
|
float opacity = random.NextSingle();
|
|
var fog = NextColour(random);
|
|
float fogFactor = random.NextSingle();
|
|
|
|
Vector3 twoDrawOrder = RetailDetailTextureContract.ExpectedFogged(
|
|
baseColour, detail, opacity, fog, fogFactor);
|
|
Vector3 retailOrder = Vector3.Lerp(
|
|
RetailDetailTextureContract.Expected(baseColour, detail, opacity),
|
|
fog,
|
|
fogFactor);
|
|
|
|
Assert.True(
|
|
MathF.Abs(twoDrawOrder.X - retailOrder.X) < 1e-6f,
|
|
$"R: two-draw {twoDrawOrder.X} vs retail {retailOrder.X} (sample {i})");
|
|
Assert.True(
|
|
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) =>
|
|
new(random.NextSingle(), random.NextSingle(), random.NextSingle());
|
|
}
|