feat(core): SurfaceDecoder — add PFID_P8, PFID_R8G8B8, and PFID_X8R8G8B8

Entities using 8-bit palette-indexed textures (PFID_P8), uncompressed 24-bit
RGB surfaces (PFID_R8G8B8), and 32-bit packed-BGR surfaces (PFID_X8R8G8B8)
were all rendering as solid magenta. PFID_P8 uses the same palette-lookup and
isClipMap (indices 0..7 → fully transparent) convention as the existing
INDEX16 decoder, reading one byte per pixel instead of two. PFID_R8G8B8 and
PFID_X8R8G8B8 decode on-disk B,G,R[,X] byte order to R,G,B,255 RGBA8 output
for OpenGL PixelFormat.Rgba upload; the X padding byte in X8R8G8B8 is
discarded rather than forwarded as alpha.

168 tests green (85 AcDream.Core.Tests + 83 AcDream.Core.Net.Tests), including
9 new SurfaceDecoder tests covering correct channel mapping, clipmap
transparency, truncated-data fallback, and palette-missing fallback for P8.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-11 19:23:57 +02:00
parent 979802c49c
commit d379a75984
2 changed files with 273 additions and 0 deletions

View file

@ -222,4 +222,184 @@ public class SurfaceDecoderTests
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);
}
}