feat(core): add SurfaceDecoder for A8R8G8B8 and BCn formats
This commit is contained in:
parent
f915a13263
commit
dbf913ebb4
4 changed files with 170 additions and 0 deletions
10
src/AcDream.Core/Textures/DecodedTexture.cs
Normal file
10
src/AcDream.Core/Textures/DecodedTexture.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace AcDream.Core.Textures;
|
||||||
|
|
||||||
|
public sealed record DecodedTexture(byte[] Rgba8, int Width, int Height)
|
||||||
|
{
|
||||||
|
/// <summary>1x1 magenta fallback for missing/unsupported textures.</summary>
|
||||||
|
public static readonly DecodedTexture Magenta = new(
|
||||||
|
Rgba8: [0xFF, 0x00, 0xFF, 0xFF],
|
||||||
|
Width: 1,
|
||||||
|
Height: 1);
|
||||||
|
}
|
||||||
71
src/AcDream.Core/Textures/SurfaceDecoder.cs
Normal file
71
src/AcDream.Core/Textures/SurfaceDecoder.cs
Normal file
|
|
@ -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();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode a RenderSurface's pixel bytes into RGBA8. Returns <see cref="DecodedTexture.Magenta"/>
|
||||||
|
/// for unsupported formats, null data, or corrupt sizing.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
89
tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs
Normal file
89
tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue