test(N.3): conformance tests proving WB TextureHelpers matches our decode
Nine tests covering INDEX16 (normal + clipmap), P8, A8R8G8B8, R8G8B8, A8Additive (matches our current DecodeA8), A8 non-additive (documents the divergence), R5G6B5, A4R4G4B4. All run before any substitution -- they prove equivalence, not test the substitution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1978ef9395
commit
2a491c6f92
1 changed files with 369 additions and 0 deletions
|
|
@ -0,0 +1,369 @@
|
||||||
|
using Chorizite.OpenGLSDLBackend.Lib;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Textures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Conformance tests proving byte-identical output between our hand-rolled
|
||||||
|
/// SurfaceDecoder paths and WorldBuilder's TextureHelpers.Fill* methods.
|
||||||
|
/// These tests run BEFORE any substitution — they prove equivalence first.
|
||||||
|
/// If a test fails, the formats diverge and that's a real finding.
|
||||||
|
/// </summary>
|
||||||
|
public class TextureDecodeConformanceTests
|
||||||
|
{
|
||||||
|
// ---- helpers ---------------------------------------------------------------
|
||||||
|
|
||||||
|
private static Palette MakePalette(params ColorARGB[] colors)
|
||||||
|
{
|
||||||
|
var pal = new Palette();
|
||||||
|
foreach (var c in colors)
|
||||||
|
pal.Colors.Add(c);
|
||||||
|
return pal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ColorARGB Rgba(byte r, byte g, byte b, byte a = 0xFF)
|
||||||
|
=> new ColorARGB { Red = r, Green = g, Blue = b, Alpha = a };
|
||||||
|
|
||||||
|
// Inline our current DecodeIndex16 logic for the conformance baseline.
|
||||||
|
private static byte[] OurDecodeIndex16(byte[] src, Palette palette, int width, int height, bool isClipMap = false)
|
||||||
|
{
|
||||||
|
var rgba = new byte[width * height * 4];
|
||||||
|
int paletteMax = palette.Colors.Count - 1;
|
||||||
|
for (int i = 0; i < width * height; i++)
|
||||||
|
{
|
||||||
|
int s = i * 2;
|
||||||
|
ushort idx = (ushort)(src[s] | (src[s + 1] << 8));
|
||||||
|
if (idx > paletteMax) idx = 0;
|
||||||
|
var c = palette.Colors[idx];
|
||||||
|
int d = i * 4;
|
||||||
|
if (isClipMap && idx < 8)
|
||||||
|
{
|
||||||
|
rgba[d + 0] = 0;
|
||||||
|
rgba[d + 1] = 0;
|
||||||
|
rgba[d + 2] = 0;
|
||||||
|
rgba[d + 3] = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rgba[d + 0] = c.Red;
|
||||||
|
rgba[d + 1] = c.Green;
|
||||||
|
rgba[d + 2] = c.Blue;
|
||||||
|
rgba[d + 3] = c.Alpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline our current DecodeP8 logic.
|
||||||
|
private static byte[] OurDecodeP8(byte[] src, Palette palette, int width, int height, bool isClipMap = false)
|
||||||
|
{
|
||||||
|
var rgba = new byte[width * height * 4];
|
||||||
|
int paletteMax = palette.Colors.Count - 1;
|
||||||
|
for (int i = 0; i < width * height; i++)
|
||||||
|
{
|
||||||
|
int idx = src[i];
|
||||||
|
if (idx > paletteMax) idx = 0;
|
||||||
|
var c = palette.Colors[idx];
|
||||||
|
int d = i * 4;
|
||||||
|
if (isClipMap && idx < 8)
|
||||||
|
{
|
||||||
|
rgba[d + 0] = 0;
|
||||||
|
rgba[d + 1] = 0;
|
||||||
|
rgba[d + 2] = 0;
|
||||||
|
rgba[d + 3] = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rgba[d + 0] = c.Red;
|
||||||
|
rgba[d + 1] = c.Green;
|
||||||
|
rgba[d + 2] = c.Blue;
|
||||||
|
rgba[d + 3] = c.Alpha;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline our current DecodeA8R8G8B8 logic (BGRA on-disk → RGBA).
|
||||||
|
private static byte[] OurDecodeA8R8G8B8(byte[] src, int width, int height)
|
||||||
|
{
|
||||||
|
var rgba = new byte[width * height * 4];
|
||||||
|
for (int i = 0; i < width * height; i++)
|
||||||
|
{
|
||||||
|
int s = i * 4;
|
||||||
|
rgba[s + 0] = src[s + 2]; // R
|
||||||
|
rgba[s + 1] = src[s + 1]; // G
|
||||||
|
rgba[s + 2] = src[s + 0]; // B
|
||||||
|
rgba[s + 3] = src[s + 3]; // A
|
||||||
|
}
|
||||||
|
return rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline our current DecodeR8G8B8 logic (BGR on-disk → RGBA with A=255).
|
||||||
|
private static byte[] OurDecodeR8G8B8(byte[] src, int width, int height)
|
||||||
|
{
|
||||||
|
var rgba = new byte[width * height * 4];
|
||||||
|
for (int i = 0; i < width * height; i++)
|
||||||
|
{
|
||||||
|
int s = i * 3;
|
||||||
|
int d = i * 4;
|
||||||
|
rgba[d + 0] = src[s + 2]; // R
|
||||||
|
rgba[d + 1] = src[s + 1]; // G
|
||||||
|
rgba[d + 2] = src[s + 0]; // B
|
||||||
|
rgba[d + 3] = 0xFF; // A = opaque
|
||||||
|
}
|
||||||
|
return rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline our current DecodeA8 logic (R=G=B=A=val — "additive" mode).
|
||||||
|
private static byte[] OurDecodeA8(byte[] src, int width, int height)
|
||||||
|
{
|
||||||
|
var rgba = new byte[width * height * 4];
|
||||||
|
for (int i = 0; i < width * height; i++)
|
||||||
|
{
|
||||||
|
byte a = src[i];
|
||||||
|
int d = i * 4;
|
||||||
|
rgba[d + 0] = a;
|
||||||
|
rgba[d + 1] = a;
|
||||||
|
rgba[d + 2] = a;
|
||||||
|
rgba[d + 3] = a;
|
||||||
|
}
|
||||||
|
return rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- tests -----------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 1: INDEX16 normal mode — 2×2 image with two palette entries.
|
||||||
|
/// WB's FillIndex16 and our DecodeIndex16 must produce identical RGBA bytes.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillIndex16_MatchesOurDecodeIndex16()
|
||||||
|
{
|
||||||
|
// 2×2 INDEX16: pixels 0,1,1,0 (indices into a 2-color palette)
|
||||||
|
var src = new byte[]
|
||||||
|
{
|
||||||
|
0x00, 0x00, // pixel(0,0) → palette index 0
|
||||||
|
0x01, 0x00, // pixel(1,0) → palette index 1
|
||||||
|
0x01, 0x00, // pixel(0,1) → palette index 1
|
||||||
|
0x00, 0x00, // pixel(1,1) → palette index 0
|
||||||
|
};
|
||||||
|
var palette = MakePalette(
|
||||||
|
Rgba(0xFF, 0x00, 0x00), // index 0 = red
|
||||||
|
Rgba(0x00, 0x00, 0xFF) // index 1 = blue
|
||||||
|
);
|
||||||
|
|
||||||
|
var expected = OurDecodeIndex16(src, palette, 2, 2);
|
||||||
|
|
||||||
|
var actual = new byte[2 * 2 * 4];
|
||||||
|
TextureHelpers.FillIndex16(src, palette, actual, 2, 2);
|
||||||
|
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 2: INDEX16 clipmap mode — indices below 8 must produce transparent pixels.
|
||||||
|
/// Both implementations share the same clipmap alpha-key convention from retail ACViewer.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillIndex16_ClipMap_MatchesOurClipMapBehavior()
|
||||||
|
{
|
||||||
|
// 4×1 INDEX16: indices 0, 1, 7, 8
|
||||||
|
// In clipmap mode, indices 0..7 → transparent; index 8 → palette color.
|
||||||
|
var src = new byte[]
|
||||||
|
{
|
||||||
|
0x00, 0x00, // index 0 → transparent
|
||||||
|
0x01, 0x00, // index 1 → transparent
|
||||||
|
0x07, 0x00, // index 7 → transparent
|
||||||
|
0x08, 0x00, // index 8 → opaque
|
||||||
|
};
|
||||||
|
// Build a 16-entry palette so indices 0–8 are all valid.
|
||||||
|
var palette = new Palette();
|
||||||
|
for (int i = 0; i < 16; i++)
|
||||||
|
palette.Colors.Add(Rgba(0xAA, 0xBB, 0xCC));
|
||||||
|
|
||||||
|
var expected = OurDecodeIndex16(src, palette, 4, 1, isClipMap: true);
|
||||||
|
|
||||||
|
var actual = new byte[4 * 1 * 4];
|
||||||
|
TextureHelpers.FillIndex16(src, palette, actual, 4, 1, isClipMap: true);
|
||||||
|
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 3: P8 (8-bit palette index) — 2×2 image.
|
||||||
|
/// WB FillP8 and our DecodeP8 must produce identical RGBA output.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillP8_MatchesOurDecodeP8()
|
||||||
|
{
|
||||||
|
// 2×2 P8: bytes are direct palette indices
|
||||||
|
var src = new byte[] { 0x00, 0x01, 0x01, 0x00 };
|
||||||
|
var palette = MakePalette(
|
||||||
|
Rgba(0x10, 0x20, 0x30), // index 0
|
||||||
|
Rgba(0x40, 0x50, 0x60) // index 1
|
||||||
|
);
|
||||||
|
|
||||||
|
var expected = OurDecodeP8(src, palette, 2, 2);
|
||||||
|
|
||||||
|
var actual = new byte[2 * 2 * 4];
|
||||||
|
TextureHelpers.FillP8(src, palette, actual, 2, 2);
|
||||||
|
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 4: A8R8G8B8 (BGRA on-disk → RGBA) — 2×1 image.
|
||||||
|
/// WB FillA8R8G8B8 and our DecodeA8R8G8B8 both swap B↔R.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillA8R8G8B8_MatchesOurDecodeA8R8G8B8()
|
||||||
|
{
|
||||||
|
// On-disk layout: B, G, R, A per pixel
|
||||||
|
var src = new byte[]
|
||||||
|
{
|
||||||
|
0x00, 0x00, 0xFF, 0xFF, // pixel 0: B=0, G=0, R=255, A=255 → red
|
||||||
|
0xFF, 0x00, 0x00, 0x80, // pixel 1: B=255, G=0, R=0, A=128 → blue, semi-transparent
|
||||||
|
};
|
||||||
|
|
||||||
|
var expected = OurDecodeA8R8G8B8(src, 2, 1);
|
||||||
|
|
||||||
|
var actual = new byte[2 * 1 * 4];
|
||||||
|
TextureHelpers.FillA8R8G8B8(src, actual, 2, 1);
|
||||||
|
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 5: R8G8B8 (BGR on-disk → RGBA, alpha forced 255) — 2×1 image.
|
||||||
|
/// Both implementations output R,G,B,255 for each 3-byte BGR triple.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillR8G8B8_MatchesOurDecodeR8G8B8()
|
||||||
|
{
|
||||||
|
// On-disk layout: B, G, R per pixel (24-bit BGR)
|
||||||
|
var src = new byte[]
|
||||||
|
{
|
||||||
|
0x00, 0x00, 0xFF, // pixel 0: B=0, G=0, R=255 → red
|
||||||
|
0x00, 0xFF, 0x00, // pixel 1: B=0, G=255, R=0 → green
|
||||||
|
};
|
||||||
|
|
||||||
|
var expected = OurDecodeR8G8B8(src, 2, 1);
|
||||||
|
|
||||||
|
var actual = new byte[2 * 1 * 4];
|
||||||
|
TextureHelpers.FillR8G8B8(src, actual, 2, 1);
|
||||||
|
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 6: A8 in additive mode — FillA8Additive replicates the byte into all four
|
||||||
|
/// channels (R=G=B=A=val). This is identical to our current DecodeA8 behavior,
|
||||||
|
/// which is used for terrain blending alpha masks.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillA8Additive_MatchesOurDecodeA8()
|
||||||
|
{
|
||||||
|
// 4×1 single-byte-per-pixel alpha values
|
||||||
|
var src = new byte[] { 0x00, 0x40, 0x80, 0xFF };
|
||||||
|
|
||||||
|
var expected = OurDecodeA8(src, 4, 1);
|
||||||
|
|
||||||
|
var actual = new byte[4 * 1 * 4];
|
||||||
|
TextureHelpers.FillA8Additive(src, actual, 4, 1);
|
||||||
|
|
||||||
|
Assert.Equal(expected, actual);
|
||||||
|
// Spot-check: each input byte fans out to all four channels
|
||||||
|
Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x00 }, actual[0..4]);
|
||||||
|
Assert.Equal(new byte[] { 0x40, 0x40, 0x40, 0x40 }, actual[4..8]);
|
||||||
|
Assert.Equal(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }, actual[12..16]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 7: A8 non-additive (FillA8) documents WB's behavior that DIFFERS from ours.
|
||||||
|
/// WB's FillA8 sets R=G=B=255 and A=input_byte.
|
||||||
|
/// Our DecodeA8 sets R=G=B=A=input_byte (the additive mode, used for terrain blending).
|
||||||
|
/// This test proves the divergence exists and documents the WB behavior explicitly.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillA8_NonAdditive_ProducesWhitePlusAlpha()
|
||||||
|
{
|
||||||
|
var src = new byte[] { 0x00, 0x80, 0xFF }; // 3×1
|
||||||
|
|
||||||
|
var actual = new byte[3 * 1 * 4];
|
||||||
|
TextureHelpers.FillA8(src, actual, 3, 1);
|
||||||
|
|
||||||
|
// WB non-additive: R=G=B=255, A=input byte
|
||||||
|
Assert.Equal(new byte[] { 255, 255, 255, 0x00 }, actual[0..4]); // alpha=0
|
||||||
|
Assert.Equal(new byte[] { 255, 255, 255, 0x80 }, actual[4..8]); // alpha=128
|
||||||
|
Assert.Equal(new byte[] { 255, 255, 255, 0xFF }, actual[8..12]); // alpha=255
|
||||||
|
|
||||||
|
// Confirm this DIFFERS from our current DecodeA8 behavior (R=G=B=A=val).
|
||||||
|
var ourDecoded = OurDecodeA8(src, 3, 1);
|
||||||
|
Assert.NotEqual(ourDecoded, actual); // divergence is intentional — both are documented
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 8: R5G6B5 (16-bit packed RGB, no alpha) — WB format we don't implement yet.
|
||||||
|
/// Verifies the expected bit-expansion: 5-bit red → 8-bit by left-shifting 3,
|
||||||
|
/// 6-bit green → 8-bit by left-shifting 2, 5-bit blue → 8-bit by left-shifting 3.
|
||||||
|
/// Alpha is always 255.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillR5G6B5_ProducesExpectedRgba()
|
||||||
|
{
|
||||||
|
// Encode a single pixel: R=0x1F (31), G=0x3F (63), B=0x1F (31)
|
||||||
|
// Packed as 16-bit little-endian: bits 15-11=R, 10-5=G, 4-0=B
|
||||||
|
// val = (0x1F << 11) | (0x3F << 5) | 0x1F = 0xFFFF
|
||||||
|
var src = new byte[] { 0xFF, 0xFF }; // 1×1 pixel: all channels maxed
|
||||||
|
|
||||||
|
var actual = new byte[1 * 1 * 4];
|
||||||
|
TextureHelpers.FillR5G6B5(src, actual, 1, 1);
|
||||||
|
|
||||||
|
// R = (0x1F << 3) = 0xF8, G = (0x3F << 2) = 0xFC, B = (0x1F << 3) = 0xF8, A = 255
|
||||||
|
Assert.Equal((byte)0xF8, actual[0]); // R
|
||||||
|
Assert.Equal((byte)0xFC, actual[1]); // G
|
||||||
|
Assert.Equal((byte)0xF8, actual[2]); // B
|
||||||
|
Assert.Equal((byte)255, actual[3]); // A always opaque
|
||||||
|
|
||||||
|
// Test a second pixel: pure red = R=31, G=0, B=0
|
||||||
|
// val = (0x1F << 11) = 0xF800
|
||||||
|
var srcRed = new byte[] { 0x00, 0xF8 }; // little-endian 0xF800
|
||||||
|
var actualRed = new byte[4];
|
||||||
|
TextureHelpers.FillR5G6B5(srcRed, actualRed, 1, 1);
|
||||||
|
Assert.Equal((byte)0xF8, actualRed[0]); // R = 31 << 3 = 0xF8
|
||||||
|
Assert.Equal((byte)0x00, actualRed[1]); // G = 0
|
||||||
|
Assert.Equal((byte)0x00, actualRed[2]); // B = 0
|
||||||
|
Assert.Equal((byte)255, actualRed[3]); // A
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test 9: A4R4G4B4 (16-bit packed ARGB, 4 bits per channel) — WB format we don't implement yet.
|
||||||
|
/// Each 4-bit value is expanded to 8-bit by multiplying by 17 (0x11),
|
||||||
|
/// so 0xF → 255, 0x8 → 136, 0x0 → 0.
|
||||||
|
/// Bit layout: val bits 15-12=A, 11-8=R, 7-4=G, 3-0=B.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void FillA4R4G4B4_ProducesExpectedRgba()
|
||||||
|
{
|
||||||
|
// Encode one pixel: A=0xF(255), R=0xA(170), G=0x5(85), B=0x0(0)
|
||||||
|
// val = (0xF << 12) | (0xA << 8) | (0x5 << 4) | 0x0 = 0xFA50
|
||||||
|
// little-endian bytes: 0x50, 0xFA
|
||||||
|
var src = new byte[] { 0x50, 0xFA }; // 1×1
|
||||||
|
|
||||||
|
var actual = new byte[1 * 1 * 4];
|
||||||
|
TextureHelpers.FillA4R4G4B4(src, actual, 1, 1);
|
||||||
|
|
||||||
|
// R = 0xA * 17 = 170, G = 0x5 * 17 = 85, B = 0x0 * 17 = 0, A = 0xF * 17 = 255
|
||||||
|
Assert.Equal((byte)(0xA * 17), actual[0]); // R = 170
|
||||||
|
Assert.Equal((byte)(0x5 * 17), actual[1]); // G = 85
|
||||||
|
Assert.Equal((byte)(0x0 * 17), actual[2]); // B = 0
|
||||||
|
Assert.Equal((byte)(0xF * 17), actual[3]); // A = 255
|
||||||
|
|
||||||
|
// Also test the zero case: all channels 0
|
||||||
|
var srcZero = new byte[] { 0x00, 0x00 };
|
||||||
|
var actualZero = new byte[4];
|
||||||
|
TextureHelpers.FillA4R4G4B4(srcZero, actualZero, 1, 1);
|
||||||
|
Assert.Equal(new byte[] { 0, 0, 0, 0 }, actualZero);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue