acdream/tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs
Erik a8459eecbb feat(core+app): alpha atlas loading (Phase 3c.2)
Loads AC's terrain blending alpha masks into a second GL_TEXTURE_2D_ARRAY
alongside the existing terrain atlas. The alpha atlas is built but not
yet sampled by any shader — that wiring lands in Phase 3c.4.

SurfaceDecoder additions:
  - Handles PFID_A8 (generic single-byte-alpha) by replicating each
    alpha byte into all four RGBA channels
  - Same branch handles PFID_CUSTOM_LSCAPE_ALPHA (0xF4), AC's landscape-
    specific alpha format — the bit layout is identical, just a different
    format ID to distinguish the asset class in the dats. I only found
    this by adding a diagnostic in the first iteration (initial attempt
    returned Magenta for every alpha map because I only wired PFID_A8)
  - 3 new tests: 2x2 A8 round-trip, short-source fallback, and a
    CUSTOM_LSCAPE_ALPHA test verifying it's routed through the same path

TerrainAtlas additions:
  - New GlAlphaTexture property plus CornerAlphaLayers / SideAlphaLayers
    / RoadAlphaLayers index lists so the coming BuildSurface port can
    cite atlas layers by source category
  - BuildAlphaAtlas walks TexMerge.CornerTerrainMaps, SideTerrainMaps,
    RoadMaps and uploads each decoded mask as a layer in insertion
    order; categories carry their atlas-layer index in the respective
    list
  - Fallback handling (single-layer white) when TexMerge is missing or
    every map fails to decode
  - Alpha atlas uses ClampToEdge wrap so repeating tile sampling at
    mask boundaries doesn't produce seams
  - Dispose() now cleans up both textures

On Holtburg's region the log prints:
  TerrainAtlas: 33 terrain layers at 512x512
  AlphaAtlas:    8 layers at 512x512  (corners=4, sides=1, roads=3)

Tests: 61/61 passing. No visual change expected this commit (shader
still ignores Data0..3 and the alpha sampler).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:45:40 +02:00

225 lines
7.2 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_ExpandsSingleByteToRgbaWithAlphaInAllChannels()
{
// PFID_A8 is single-byte-per-pixel alpha. AC terrain blending alpha maps
// are stored this way. WorldBuilder's GetExpandedAlphaTexture replicates
// the byte into all four RGBA channels so fragment shaders can read the
// blend value from any channel (convention: the 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 (b, b, b, b) in RGBA output
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.
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[]
{
0x10, 0x10, 0x10, 0x10,
0x20, 0x20, 0x20, 0x20,
0x30, 0x30, 0x30, 0x30,
0x40, 0x40, 0x40, 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]);
}
}