Decode_A8_ExpandsSingleByteToRgbaWithAlphaInAllChannels renamed to Decode_A8_NonAdditive_ProducesWhitePlusAlpha with updated expectations (R=G=B=255, A=val) matching the new default isAdditive:false WB semantics. Decode_CustomLscapeAlpha_TreatedIdenticallyToA8 updated to the same non-additive expectation (255,255,255,val). New test Decode_A8_Additive_ReplicatesByteToAllChannels documents the isAdditive:true path (R=G=B=A=val) used by TerrainAtlas alpha maps. 8 pre-existing failures unchanged. 883 pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
430 lines
14 KiB
C#
430 lines
14 KiB
C#
using AcDream.Core.Textures;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.Core.Tests.Textures;
|
|
|
|
public class SurfaceDecoderTests
|
|
{
|
|
[Fact]
|
|
public void Decode_A8R8G8B8_ConvertsToRgba8()
|
|
{
|
|
// Source format is B, G, R, A in memory (little-endian ARGB).
|
|
// One 2x2 image: red, green, blue, white pixels.
|
|
var src = new byte[]
|
|
{
|
|
0x00, 0x00, 0xFF, 0xFF, // red (B=0, G=0, R=255, A=255)
|
|
0x00, 0xFF, 0x00, 0xFF, // green
|
|
0xFF, 0x00, 0x00, 0xFF, // blue
|
|
0xFF, 0xFF, 0xFF, 0xFF, // white
|
|
};
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 2,
|
|
Format = PixelFormat.PFID_A8R8G8B8,
|
|
SourceData = src,
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Equal(2, decoded.Width);
|
|
Assert.Equal(2, decoded.Height);
|
|
Assert.Equal(16, decoded.Rgba8.Length); // 2*2*4
|
|
// red pixel, in RGBA: 255, 0, 0, 255
|
|
Assert.Equal(0xFF, decoded.Rgba8[0]);
|
|
Assert.Equal(0x00, decoded.Rgba8[1]);
|
|
Assert.Equal(0x00, decoded.Rgba8[2]);
|
|
Assert.Equal(0xFF, decoded.Rgba8[3]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_UnsupportedFormat_ReturnsMagenta()
|
|
{
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 4,
|
|
Height = 4,
|
|
Format = PixelFormat.PFID_INDEX16, // not implemented path
|
|
SourceData = new byte[32],
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Same(DecodedTexture.Magenta, decoded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_A8_NonAdditive_ProducesWhitePlusAlpha()
|
|
{
|
|
// Default (isAdditive: false) = WB FillA8 semantics: R=G=B=255, A=val.
|
|
// Used for non-additive entity surfaces where A8 is a pure alpha channel.
|
|
var src = new byte[] { 0x00, 0x40, 0x80, 0xFF }; // 2x2 image
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 2,
|
|
Format = PixelFormat.PFID_A8,
|
|
SourceData = src,
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Equal(2, decoded.Width);
|
|
Assert.Equal(2, decoded.Height);
|
|
Assert.Equal(16, decoded.Rgba8.Length);
|
|
// Each input byte expands to (255, 255, 255, val) — white with varying alpha
|
|
Assert.Equal(new byte[]
|
|
{
|
|
255, 255, 255, 0x00,
|
|
255, 255, 255, 0x40,
|
|
255, 255, 255, 0x80,
|
|
255, 255, 255, 0xFF,
|
|
}, decoded.Rgba8);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_A8_Additive_ReplicatesByteToAllChannels()
|
|
{
|
|
// isAdditive=true = WB FillA8Additive semantics: R=G=B=A=val.
|
|
// Used for terrain blending alpha masks (TerrainAtlas always passes isAdditive:true).
|
|
var src = new byte[] { 0x00, 0x40, 0x80, 0xFF }; // 2x2 image
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 2,
|
|
Format = PixelFormat.PFID_A8,
|
|
SourceData = src,
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true);
|
|
|
|
Assert.Equal(16, decoded.Rgba8.Length);
|
|
// Each input byte fans out to all four channels
|
|
Assert.Equal(new byte[]
|
|
{
|
|
0x00, 0x00, 0x00, 0x00,
|
|
0x40, 0x40, 0x40, 0x40,
|
|
0x80, 0x80, 0x80, 0x80,
|
|
0xFF, 0xFF, 0xFF, 0xFF,
|
|
}, decoded.Rgba8);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_CustomLscapeAlpha_TreatedIdenticallyToA8()
|
|
{
|
|
// PFID_CUSTOM_LSCAPE_ALPHA (0xF4) is AC's custom format for terrain
|
|
// blending alpha maps. Pixel layout is identical to PFID_A8 — one
|
|
// byte of alpha per pixel — so the decoder routes both through the
|
|
// same DecodeA8 implementation. Default (isAdditive:false) → R=G=B=255, A=val.
|
|
var src = new byte[] { 0x10, 0x20, 0x30, 0x40 }; // 2x2
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 2,
|
|
Format = PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA,
|
|
SourceData = src,
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Equal(16, decoded.Rgba8.Length);
|
|
Assert.Equal(new byte[]
|
|
{
|
|
255, 255, 255, 0x10,
|
|
255, 255, 255, 0x20,
|
|
255, 255, 255, 0x30,
|
|
255, 255, 255, 0x40,
|
|
}, decoded.Rgba8);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_A8_WithShortSourceData_ReturnsMagenta()
|
|
{
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 4,
|
|
Height = 4,
|
|
Format = PixelFormat.PFID_A8,
|
|
SourceData = new byte[8], // expects 16
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Same(DecodedTexture.Magenta, decoded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_NullSourceData_ReturnsMagenta()
|
|
{
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 4,
|
|
Height = 4,
|
|
Format = PixelFormat.PFID_A8R8G8B8,
|
|
SourceData = null!,
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Same(DecodedTexture.Magenta, decoded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_TruncatedA8R8G8B8_ReturnsMagenta()
|
|
{
|
|
// Buffer too small for width*height*4.
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 2,
|
|
Format = PixelFormat.PFID_A8R8G8B8,
|
|
SourceData = new byte[8], // should be 16
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Same(DecodedTexture.Magenta, decoded);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeSolidColor_Opaque_PreservesAlpha()
|
|
{
|
|
var color = new ColorARGB { Alpha = 0xFF, Red = 0x11, Green = 0x22, Blue = 0x33 };
|
|
|
|
var decoded = SurfaceDecoder.DecodeSolidColor(color, translucency: 0f);
|
|
|
|
Assert.Equal(1, decoded.Width);
|
|
Assert.Equal(1, decoded.Height);
|
|
Assert.Equal(new byte[] { 0x11, 0x22, 0x33, 0xFF }, decoded.Rgba8);
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeSolidColor_FullyTranslucent_AlphaGoesToZero()
|
|
{
|
|
// Surfaces marked Base1Solid + Translucent with Translucency=1.0 are
|
|
// AC's convention for "invisible placeholder surfaces" — the engine renders
|
|
// them as nothing. Alpha must go to 0 so the mesh shader's discard rule
|
|
// makes them invisible.
|
|
var color = new ColorARGB { Alpha = 0xFF, Red = 0xC8, Green = 0xC8, Blue = 0xC8 };
|
|
|
|
var decoded = SurfaceDecoder.DecodeSolidColor(color, translucency: 1f);
|
|
|
|
Assert.Equal(0, decoded.Rgba8[3]); // alpha must be zero
|
|
}
|
|
|
|
[Fact]
|
|
public void DecodeIndex16_ClipMap_ZerosAlphaForLowIndices()
|
|
{
|
|
// Build a 4x1 INDEX16 surface with indices 0, 1, 7, 8.
|
|
// On a clipmap surface, indices 0..7 should be fully transparent and
|
|
// index 8 should render with its palette color.
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 4,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_INDEX16,
|
|
SourceData = new byte[]
|
|
{
|
|
0x00, 0x00, // index 0
|
|
0x01, 0x00, // index 1
|
|
0x07, 0x00, // index 7
|
|
0x08, 0x00, // index 8
|
|
},
|
|
};
|
|
var palette = new Palette();
|
|
for (int i = 0; i < 16; i++)
|
|
palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xAA, Green = 0xBB, Blue = 0xCC });
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette, isClipMap: true);
|
|
|
|
// Pixels 0, 1, 2 (indices 0, 1, 7) should be fully transparent.
|
|
Assert.Equal(0, decoded.Rgba8[3]); // pixel 0 alpha
|
|
Assert.Equal(0, decoded.Rgba8[7]); // pixel 1 alpha
|
|
Assert.Equal(0, decoded.Rgba8[11]); // pixel 2 alpha
|
|
// Pixel 3 (index 8) should have the palette alpha.
|
|
Assert.Equal(0xFF, decoded.Rgba8[15]);
|
|
Assert.Equal(0xAA, decoded.Rgba8[12]);
|
|
}
|
|
|
|
// ---- PFID_P8 tests -------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Decode_P8_LooksUpPaletteForEachByte()
|
|
{
|
|
// 2x1 surface: pixel 0 → palette index 0 (red), pixel 1 → palette index 1 (blue).
|
|
var palette = new Palette();
|
|
palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xFF, Green = 0x00, Blue = 0x00 }); // index 0 = red
|
|
palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0x00, Green = 0x00, Blue = 0xFF }); // index 1 = blue
|
|
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_P8,
|
|
SourceData = new byte[] { 0x00, 0x01 }, // indices
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette);
|
|
|
|
Assert.Equal(8, decoded.Rgba8.Length); // 2 pixels * 4 channels
|
|
// Pixel 0: red
|
|
Assert.Equal(new byte[] { 0xFF, 0x00, 0x00, 0xFF }, decoded.Rgba8[0..4]);
|
|
// Pixel 1: blue
|
|
Assert.Equal(new byte[] { 0x00, 0x00, 0xFF, 0xFF }, decoded.Rgba8[4..8]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_P8_ClipMap_ZerosAlphaForLowIndices()
|
|
{
|
|
// 4x1 surface with indices 0, 3, 7, 8.
|
|
// isClipMap=true → indices 0..7 should be fully transparent; index 8 opaque.
|
|
var palette = new Palette();
|
|
for (int i = 0; i < 16; i++)
|
|
palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xCC, Green = 0xDD, Blue = 0xEE });
|
|
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 4,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_P8,
|
|
SourceData = new byte[] { 0x00, 0x03, 0x07, 0x08 },
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette, isClipMap: true);
|
|
|
|
// Indices 0, 3, 7 should be transparent.
|
|
Assert.Equal(0, decoded.Rgba8[3]); // pixel 0 alpha
|
|
Assert.Equal(0, decoded.Rgba8[7]); // pixel 1 alpha
|
|
Assert.Equal(0, decoded.Rgba8[11]); // pixel 2 alpha
|
|
// Index 8 should be opaque with palette color.
|
|
Assert.Equal(0xFF, decoded.Rgba8[15]);
|
|
Assert.Equal(0xCC, decoded.Rgba8[12]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_P8_WithoutPalette_ReturnsMagenta()
|
|
{
|
|
// P8 without palette passed → falls through to magenta.
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_P8,
|
|
SourceData = new byte[] { 0x00, 0x01 },
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Same(DecodedTexture.Magenta, decoded);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_P8_TruncatedData_ReturnsMagenta()
|
|
{
|
|
var palette = new Palette();
|
|
palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xAA, Green = 0xBB, Blue = 0xCC });
|
|
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 4,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_P8,
|
|
SourceData = new byte[] { 0x00, 0x00 }, // expects 4 bytes
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette);
|
|
|
|
Assert.Same(DecodedTexture.Magenta, decoded);
|
|
}
|
|
|
|
// ---- PFID_R8G8B8 tests ---------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Decode_R8G8B8_ConvertsToRgba8WithOpaqueAlpha()
|
|
{
|
|
// PFID_R8G8B8 is stored on disk as B,G,R (little-endian 24-bit BGR).
|
|
// 2x1 surface: first pixel = red (B=0,G=0,R=255), second = green (B=0,G=255,R=0).
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_R8G8B8,
|
|
SourceData = new byte[]
|
|
{
|
|
0x00, 0x00, 0xFF, // B=0, G=0, R=255 → red
|
|
0x00, 0xFF, 0x00, // B=0, G=255, R=0 → green
|
|
},
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Equal(8, decoded.Rgba8.Length);
|
|
// Red pixel → R=255, G=0, B=0, A=255
|
|
Assert.Equal(new byte[] { 0xFF, 0x00, 0x00, 0xFF }, decoded.Rgba8[0..4]);
|
|
// Green pixel → R=0, G=255, B=0, A=255
|
|
Assert.Equal(new byte[] { 0x00, 0xFF, 0x00, 0xFF }, decoded.Rgba8[4..8]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_R8G8B8_TruncatedData_ReturnsMagenta()
|
|
{
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_R8G8B8,
|
|
SourceData = new byte[] { 0x00, 0x00 }, // expects 6 bytes
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Same(DecodedTexture.Magenta, decoded);
|
|
}
|
|
|
|
// ---- PFID_X8R8G8B8 tests -------------------------------------------------
|
|
|
|
[Fact]
|
|
public void Decode_X8R8G8B8_ConvertsToRgba8DiscardingXByte()
|
|
{
|
|
// PFID_X8R8G8B8 is stored on disk as B,G,R,X (DirectX little-endian 32-bit).
|
|
// The X byte is unused padding — NOT alpha. Output alpha must be 255.
|
|
// 2x1: first pixel = blue (B=255,G=0,R=0,X=0xDE), second = white (B=255,G=255,R=255,X=0xAD).
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_X8R8G8B8,
|
|
SourceData = new byte[]
|
|
{
|
|
0xFF, 0x00, 0x00, 0xDE, // B=255, G=0, R=0, X=0xDE → blue, alpha forced 255
|
|
0xFF, 0xFF, 0xFF, 0xAD, // B=255, G=255, R=255, X=0xAD → white, alpha forced 255
|
|
},
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Equal(8, decoded.Rgba8.Length);
|
|
// Blue pixel → R=0, G=0, B=255, A=255 (X byte discarded)
|
|
Assert.Equal(new byte[] { 0x00, 0x00, 0xFF, 0xFF }, decoded.Rgba8[0..4]);
|
|
// White pixel → R=255, G=255, B=255, A=255 (X byte discarded)
|
|
Assert.Equal(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }, decoded.Rgba8[4..8]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Decode_X8R8G8B8_TruncatedData_ReturnsMagenta()
|
|
{
|
|
var rs = new RenderSurface
|
|
{
|
|
Width = 2,
|
|
Height = 1,
|
|
Format = PixelFormat.PFID_X8R8G8B8,
|
|
SourceData = new byte[] { 0xFF, 0x00, 0x00, 0xDE }, // expects 8 bytes (2 pixels)
|
|
};
|
|
|
|
var decoded = SurfaceDecoder.DecodeRenderSurface(rs);
|
|
|
|
Assert.Same(DecodedTexture.Magenta, decoded);
|
|
}
|
|
}
|