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:
parent
979802c49c
commit
d379a75984
2 changed files with 273 additions and 0 deletions
|
|
@ -34,11 +34,14 @@ public static class SurfaceDecoder
|
|||
{
|
||||
return rs.Format switch
|
||||
{
|
||||
PixelFormat.PFID_R8G8B8 => DecodeR8G8B8(rs),
|
||||
PixelFormat.PFID_A8R8G8B8 => DecodeA8R8G8B8(rs),
|
||||
PixelFormat.PFID_X8R8G8B8 => DecodeX8R8G8B8(rs),
|
||||
PixelFormat.PFID_DXT1 => DecodeBc(rs, CompressionFormat.Bc1),
|
||||
PixelFormat.PFID_DXT3 => DecodeBc(rs, CompressionFormat.Bc2),
|
||||
PixelFormat.PFID_DXT5 => DecodeBc(rs, CompressionFormat.Bc3),
|
||||
PixelFormat.PFID_A8 or PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA => DecodeA8(rs),
|
||||
PixelFormat.PFID_P8 when palette is not null => DecodeP8(rs, palette, isClipMap),
|
||||
PixelFormat.PFID_INDEX16 when palette is not null => DecodeIndex16(rs, palette, isClipMap),
|
||||
_ => DecodedTexture.Magenta,
|
||||
};
|
||||
|
|
@ -152,6 +155,96 @@ public static class SurfaceDecoder
|
|||
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode PFID_P8 (8-bit palette index, one byte per pixel) into RGBA8.
|
||||
/// This is the 8-bit sibling of PFID_INDEX16: each byte is a palette index.
|
||||
/// The <paramref name="isClipMap"/> convention (indices 0..7 → fully transparent)
|
||||
/// is identical to the INDEX16 path.
|
||||
/// </summary>
|
||||
private static DecodedTexture DecodeP8(RenderSurface rs, Palette palette, bool isClipMap)
|
||||
{
|
||||
int expectedBytes = rs.Width * rs.Height;
|
||||
if (rs.SourceData.Length < expectedBytes || palette.Colors.Count == 0)
|
||||
return DecodedTexture.Magenta;
|
||||
|
||||
var rgba = new byte[rs.Width * rs.Height * 4];
|
||||
int paletteMax = palette.Colors.Count - 1;
|
||||
for (int i = 0; i < rs.Width * rs.Height; i++)
|
||||
{
|
||||
int idx = rs.SourceData[i];
|
||||
if (idx > paletteMax) idx = 0;
|
||||
var c = palette.Colors[idx];
|
||||
|
||||
int dst = i * 4;
|
||||
if (isClipMap && idx < 8)
|
||||
{
|
||||
rgba[dst + 0] = 0;
|
||||
rgba[dst + 1] = 0;
|
||||
rgba[dst + 2] = 0;
|
||||
rgba[dst + 3] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
rgba[dst + 0] = c.Red;
|
||||
rgba[dst + 1] = c.Green;
|
||||
rgba[dst + 2] = c.Blue;
|
||||
rgba[dst + 3] = c.Alpha;
|
||||
}
|
||||
}
|
||||
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode PFID_R8G8B8 (24-bit, 3 bytes per pixel) into RGBA8 with alpha=255.
|
||||
/// AC stores R8G8B8 on disk in B,G,R byte order (confirmed by ACE's
|
||||
/// GetImageColorArray: <c>byte b = reader.ReadByte(); g = ...; r = ...;</c>).
|
||||
/// Output is R,G,B,255 in RGBA8 order for OpenGL PixelFormat.Rgba upload.
|
||||
/// </summary>
|
||||
private static DecodedTexture DecodeR8G8B8(RenderSurface rs)
|
||||
{
|
||||
int expectedBytes = rs.Width * rs.Height * 3;
|
||||
if (rs.SourceData.Length < expectedBytes)
|
||||
return DecodedTexture.Magenta;
|
||||
|
||||
var rgba = new byte[rs.Width * rs.Height * 4];
|
||||
for (int i = 0; i < rs.Width * rs.Height; i++)
|
||||
{
|
||||
int src = i * 3;
|
||||
int dst = i * 4;
|
||||
// On-disk byte order: B, G, R (little-endian 24-bit BGR, same as DX PFID_R8G8B8)
|
||||
rgba[dst + 0] = rs.SourceData[src + 2]; // R
|
||||
rgba[dst + 1] = rs.SourceData[src + 1]; // G
|
||||
rgba[dst + 2] = rs.SourceData[src + 0]; // B
|
||||
rgba[dst + 3] = 0xFF; // A = opaque
|
||||
}
|
||||
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode PFID_X8R8G8B8 (32-bit, 4 bytes per pixel) into RGBA8 with alpha=255.
|
||||
/// AC stores X8R8G8B8 on disk in B,G,R,X byte order (DirectX little-endian
|
||||
/// convention: low byte = B). The X (high) byte is unused padding and is
|
||||
/// discarded — it is NOT treated as alpha. Output is R,G,B,255 for OpenGL.
|
||||
/// </summary>
|
||||
private static DecodedTexture DecodeX8R8G8B8(RenderSurface rs)
|
||||
{
|
||||
int expectedBytes = rs.Width * rs.Height * 4;
|
||||
if (rs.SourceData.Length < expectedBytes)
|
||||
return DecodedTexture.Magenta;
|
||||
|
||||
var rgba = new byte[expectedBytes];
|
||||
for (int i = 0; i < rs.Width * rs.Height; i++)
|
||||
{
|
||||
int s = i * 4;
|
||||
// On-disk byte order: B, G, R, X (little-endian 32-bit; high byte X is padding)
|
||||
rgba[s + 0] = rs.SourceData[s + 2]; // R
|
||||
rgba[s + 1] = rs.SourceData[s + 1]; // G
|
||||
rgba[s + 2] = rs.SourceData[s + 0]; // B
|
||||
rgba[s + 3] = 0xFF; // A = opaque (X byte discarded)
|
||||
}
|
||||
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
||||
}
|
||||
|
||||
private static DecodedTexture DecodeBc(RenderSurface rs, CompressionFormat format)
|
||||
{
|
||||
var pixels = BcDecoder.DecodeRaw(rs.SourceData, rs.Width, rs.Height, format);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue