diff --git a/src/AcDream.Core/Textures/.gitkeep b/src/AcDream.Core/Textures/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/AcDream.Core/Textures/DecodedTexture.cs b/src/AcDream.Core/Textures/DecodedTexture.cs new file mode 100644 index 0000000..130cecf --- /dev/null +++ b/src/AcDream.Core/Textures/DecodedTexture.cs @@ -0,0 +1,10 @@ +namespace AcDream.Core.Textures; + +public sealed record DecodedTexture(byte[] Rgba8, int Width, int Height) +{ + /// 1x1 magenta fallback for missing/unsupported textures. + public static readonly DecodedTexture Magenta = new( + Rgba8: [0xFF, 0x00, 0xFF, 0xFF], + Width: 1, + Height: 1); +} diff --git a/src/AcDream.Core/Textures/SurfaceDecoder.cs b/src/AcDream.Core/Textures/SurfaceDecoder.cs new file mode 100644 index 0000000..481b947 --- /dev/null +++ b/src/AcDream.Core/Textures/SurfaceDecoder.cs @@ -0,0 +1,71 @@ +using BCnEncoder.Decoder; +using BCnEncoder.Shared; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; + +namespace AcDream.Core.Textures; + +public static class SurfaceDecoder +{ + private static readonly BcDecoder BcDecoder = new(); + + /// + /// Decode a RenderSurface's pixel bytes into RGBA8. Returns + /// for unsupported formats, null data, or corrupt sizing. + /// + public static DecodedTexture DecodeRenderSurface(RenderSurface rs) + { + if (rs.SourceData is null || rs.Width <= 0 || rs.Height <= 0) + return DecodedTexture.Magenta; + + try + { + return rs.Format switch + { + PixelFormat.PFID_A8R8G8B8 => DecodeA8R8G8B8(rs), + PixelFormat.PFID_DXT1 => DecodeBc(rs, CompressionFormat.Bc1), + PixelFormat.PFID_DXT3 => DecodeBc(rs, CompressionFormat.Bc2), + PixelFormat.PFID_DXT5 => DecodeBc(rs, CompressionFormat.Bc3), + _ => DecodedTexture.Magenta, + }; + } + catch + { + return DecodedTexture.Magenta; + } + } + + private static DecodedTexture DecodeA8R8G8B8(RenderSurface rs) + { + int expected = rs.Width * rs.Height * 4; + if (rs.SourceData.Length < expected) + return DecodedTexture.Magenta; + + var rgba = new byte[expected]; + // Source layout per pixel: B, G, R, A → swap to R, G, B, A + for (int i = 0; i < rs.Width * rs.Height; i++) + { + int s = i * 4; + rgba[s + 0] = rs.SourceData[s + 2]; // R <- R + rgba[s + 1] = rs.SourceData[s + 1]; // G <- G + rgba[s + 2] = rs.SourceData[s + 0]; // B <- B + rgba[s + 3] = rs.SourceData[s + 3]; // A <- A + } + 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); + var rgba = new byte[rs.Width * rs.Height * 4]; + for (int i = 0; i < pixels.Length; i++) + { + int s = i * 4; + rgba[s + 0] = pixels[i].r; + rgba[s + 1] = pixels[i].g; + rgba[s + 2] = pixels[i].b; + rgba[s + 3] = pixels[i].a; + } + return new DecodedTexture(rgba, rs.Width, rs.Height); + } +} diff --git a/tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs b/tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs new file mode 100644 index 0000000..dadcb97 --- /dev/null +++ b/tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs @@ -0,0 +1,89 @@ +using AcDream.Core.Textures; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Enums; + +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_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); + } +}