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>
This commit is contained in:
parent
8b940bd038
commit
a8459eecbb
3 changed files with 324 additions and 19 deletions
|
|
@ -9,24 +9,65 @@ using GLPixelFormat = Silk.NET.OpenGL.PixelFormat;
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Builds a GL_TEXTURE_2D_ARRAY from the set of terrain types seen in the loaded
|
||||
/// landblocks, one layer per unique terrain type. LandblockMesh writes per-vertex
|
||||
/// layer indices into Vertex.TerrainLayer; the terrain fragment shader samples
|
||||
/// texture(uAtlas, vec3(uv, float(vLayer))).
|
||||
/// Holds both texture arrays the terrain renderer samples from:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>
|
||||
/// <b>Terrain atlas</b> — one GL_TEXTURE_2D_ARRAY layer per terrain type
|
||||
/// (grass, dirt, sand, forest...), sourced from
|
||||
/// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc.
|
||||
/// </description></item>
|
||||
/// <item><description>
|
||||
/// <b>Alpha atlas</b> — one GL_TEXTURE_2D_ARRAY layer per blend mask,
|
||||
/// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the
|
||||
/// same TexMerge. Used by the fragment shader to blend up to three
|
||||
/// terrain overlays and two roads on top of a base cell texture.
|
||||
/// </description></item>
|
||||
/// </list>
|
||||
/// The alpha atlas is built but not yet sampled by any shader — that wiring
|
||||
/// lands in Phase 3c.4 along with the shader rewrite.
|
||||
/// </summary>
|
||||
public sealed unsafe class TerrainAtlas : IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
public uint GlTexture { get; }
|
||||
|
||||
// --- Terrain atlas (unchanged public API from Phase 2b) ---
|
||||
public uint GlTexture { get; } // terrain atlas, kept as GlTexture for back-compat with TerrainRenderer
|
||||
public IReadOnlyDictionary<uint, uint> TerrainTypeToLayer { get; }
|
||||
public int LayerCount { get; }
|
||||
|
||||
private TerrainAtlas(GL gl, uint glTexture, IReadOnlyDictionary<uint, uint> map, int layerCount)
|
||||
// --- Alpha atlas (new in Phase 3c.2) ---
|
||||
public uint GlAlphaTexture { get; }
|
||||
public int AlphaLayerCount { get; }
|
||||
/// <summary>
|
||||
/// Layer indices in the alpha atlas for CornerTerrainMaps (typically 4 entries).
|
||||
/// Matches WorldBuilder's convention that corner alpha indices start at 0.
|
||||
/// </summary>
|
||||
public IReadOnlyList<byte> CornerAlphaLayers { get; }
|
||||
/// <summary>
|
||||
/// Layer indices in the alpha atlas for SideTerrainMaps (typically 4 entries).
|
||||
/// WorldBuilder convention: side indices start at 4.
|
||||
/// </summary>
|
||||
public IReadOnlyList<byte> SideAlphaLayers { get; }
|
||||
/// <summary>
|
||||
/// Layer indices in the alpha atlas for RoadMaps (variable count, typically ~10).
|
||||
/// </summary>
|
||||
public IReadOnlyList<byte> RoadAlphaLayers { get; }
|
||||
|
||||
private TerrainAtlas(
|
||||
GL gl,
|
||||
uint glTexture, IReadOnlyDictionary<uint, uint> map, int layerCount,
|
||||
uint glAlphaTexture, int alphaLayerCount,
|
||||
IReadOnlyList<byte> cornerLayers, IReadOnlyList<byte> sideLayers, IReadOnlyList<byte> roadLayers)
|
||||
{
|
||||
_gl = gl;
|
||||
GlTexture = glTexture;
|
||||
TerrainTypeToLayer = map;
|
||||
LayerCount = layerCount;
|
||||
GlAlphaTexture = glAlphaTexture;
|
||||
AlphaLayerCount = alphaLayerCount;
|
||||
CornerAlphaLayers = cornerLayers;
|
||||
SideAlphaLayers = sideLayers;
|
||||
RoadAlphaLayers = roadLayers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -39,17 +80,15 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
var region = dats.Get<Region>(0x13000000u)
|
||||
?? throw new InvalidOperationException("Region dat id 0x13000000 missing");
|
||||
|
||||
var terrainDesc = region.TerrainInfo?.LandSurfaces?.TexMerge?.TerrainDesc;
|
||||
var texMerge = region.TerrainInfo?.LandSurfaces?.TexMerge;
|
||||
var terrainDesc = texMerge?.TerrainDesc;
|
||||
if (terrainDesc is null || terrainDesc.Count == 0)
|
||||
{
|
||||
// Fallback: upload a single 1x1 white layer as layer 0.
|
||||
Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer");
|
||||
return BuildFallback(gl);
|
||||
}
|
||||
|
||||
// Walk TerrainDesc. Each TMTerrainDesc has a TerrainType (enum cast to uint)
|
||||
// and a TerrainTex with a QualifiedDataId<SurfaceTexture> TextureId. Decode
|
||||
// each referenced SurfaceTexture → RenderSurface → RGBA8 via SurfaceDecoder.
|
||||
// ---- Terrain atlas (unchanged Phase 2b logic) ----
|
||||
var decodedByType = new Dictionary<uint, DecodedTexture>();
|
||||
int maxW = 1, maxH = 1;
|
||||
foreach (var tmtd in terrainDesc)
|
||||
|
|
@ -84,9 +123,6 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
if (decoded.Height > maxH) maxH = decoded.Height;
|
||||
}
|
||||
|
||||
// Allocate the GL_TEXTURE_2D_ARRAY with the max dimensions seen. Textures
|
||||
// smaller than (maxW, maxH) are scaled up naively by nearest-neighbor
|
||||
// replication into a resized RGBA8 buffer. Phase 2b doesn't need mip chains.
|
||||
int layerCount = decodedByType.Count;
|
||||
uint tex = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, tex);
|
||||
|
|
@ -116,11 +152,159 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
|
||||
Console.WriteLine($"TerrainAtlas: {layerCount} layers at {maxW}x{maxH}");
|
||||
return new TerrainAtlas(gl, tex, map, layerCount);
|
||||
Console.WriteLine($"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH}");
|
||||
|
||||
// ---- Alpha atlas (new in Phase 3c.2) ----
|
||||
// texMerge is guaranteed non-null here: the early return above exited
|
||||
// if texMerge?.TerrainDesc was null.
|
||||
var (glAlpha, alphaLayerCount, cornerLayers, sideLayers, roadLayers) =
|
||||
BuildAlphaAtlas(gl, dats, texMerge!);
|
||||
|
||||
return new TerrainAtlas(
|
||||
gl,
|
||||
tex, map, layerCount,
|
||||
glAlpha, alphaLayerCount,
|
||||
cornerLayers, sideLayers, roadLayers);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load corner, side, and road alpha maps from the TexMerge into a second
|
||||
/// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures;
|
||||
/// <see cref="SurfaceDecoder.DecodeRenderSurface"/> expands each alpha byte
|
||||
/// into all four RGBA channels so the shader can sample from any channel.
|
||||
///
|
||||
/// Layers are appended in TexMerge insertion order: corners first, then
|
||||
/// sides, then roads. The returned index lists tell
|
||||
/// <c>TerrainBlending.BuildSurface</c> which layer to cite for each
|
||||
/// corner/side/road alpha source.
|
||||
/// </summary>
|
||||
private static (uint gl, int layerCount,
|
||||
IReadOnlyList<byte> corner, IReadOnlyList<byte> side, IReadOnlyList<byte> road)
|
||||
BuildAlphaAtlas(GL gl, DatCollection dats, DatReaderWriter.Types.TexMerge texMerge)
|
||||
{
|
||||
var decoded = new List<DecodedTexture>();
|
||||
var cornerLayers = new List<byte>();
|
||||
var sideLayers = new List<byte>();
|
||||
var roadLayers = new List<byte>();
|
||||
|
||||
foreach (var entry in texMerge.CornerTerrainMaps)
|
||||
{
|
||||
if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex))
|
||||
{
|
||||
cornerLayers.Add((byte)decoded.Count);
|
||||
decoded.Add(dtex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"WARN: CornerTerrainMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode");
|
||||
}
|
||||
}
|
||||
foreach (var entry in texMerge.SideTerrainMaps)
|
||||
{
|
||||
if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex))
|
||||
{
|
||||
sideLayers.Add((byte)decoded.Count);
|
||||
decoded.Add(dtex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"WARN: SideTerrainMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode");
|
||||
}
|
||||
}
|
||||
foreach (var entry in texMerge.RoadMaps)
|
||||
{
|
||||
if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex))
|
||||
{
|
||||
roadLayers.Add((byte)decoded.Count);
|
||||
decoded.Add(dtex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"WARN: RoadMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode");
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded.Count == 0)
|
||||
{
|
||||
Console.WriteLine("WARN: no alpha maps loaded; alpha atlas will be a 1x1 white fallback");
|
||||
uint fallbackAlpha = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, fallbackAlpha);
|
||||
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
fixed (byte* p = white)
|
||||
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
return (fallbackAlpha, 1, cornerLayers, sideLayers, roadLayers);
|
||||
}
|
||||
|
||||
// Alpha maps should all be uniform size (WorldBuilder asserts 512×512).
|
||||
// Fall back to the max observed so a stray mismatch doesn't crash us.
|
||||
int aMaxW = 1, aMaxH = 1;
|
||||
foreach (var d in decoded)
|
||||
{
|
||||
if (d.Width > aMaxW) aMaxW = d.Width;
|
||||
if (d.Height > aMaxH) aMaxH = d.Height;
|
||||
}
|
||||
|
||||
uint glAlpha = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, glAlpha);
|
||||
gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8,
|
||||
(uint)aMaxW, (uint)aMaxH, (uint)decoded.Count,
|
||||
0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
|
||||
for (int i = 0; i < decoded.Count; i++)
|
||||
{
|
||||
var buffer = ResizeRgba8Nearest(decoded[i], aMaxW, aMaxH);
|
||||
fixed (byte* p = buffer)
|
||||
{
|
||||
gl.TexSubImage3D(
|
||||
TextureTarget.Texture2DArray, 0,
|
||||
0, 0, i,
|
||||
(uint)aMaxW, (uint)aMaxH, 1,
|
||||
GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
}
|
||||
}
|
||||
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
|
||||
Console.WriteLine(
|
||||
$"AlphaAtlas: {decoded.Count} layers at {aMaxW}x{aMaxH} "
|
||||
+ $"(corners={cornerLayers.Count}, sides={sideLayers.Count}, roads={roadLayers.Count})");
|
||||
|
||||
return (glAlpha, decoded.Count, cornerLayers, sideLayers, roadLayers);
|
||||
}
|
||||
|
||||
private static bool TryDecodeAlphaMap(DatCollection dats, uint surfaceTextureId, out DecodedTexture decoded)
|
||||
{
|
||||
decoded = DecodedTexture.Magenta;
|
||||
|
||||
var st = dats.Get<SurfaceTexture>(surfaceTextureId);
|
||||
if (st is null || st.Textures.Count == 0)
|
||||
return false;
|
||||
|
||||
var rs = dats.Get<RenderSurface>((uint)st.Textures[0]);
|
||||
if (rs is null)
|
||||
return false;
|
||||
|
||||
// Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha
|
||||
// format) or the more generic PFID_A8; SurfaceDecoder routes both
|
||||
// through the same "replicate single byte to RGBA" path. Palette is
|
||||
// not used.
|
||||
var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null);
|
||||
if (ReferenceEquals(d, DecodedTexture.Magenta))
|
||||
return false;
|
||||
|
||||
decoded = d;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte[] ResizeRgba8Nearest(DecodedTexture src, int dstW, int dstH)
|
||||
|
|
@ -157,8 +341,25 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
return new TerrainAtlas(gl, tex, new Dictionary<uint, uint> { [0] = 0u }, 1);
|
||||
|
||||
// Fallback alpha atlas: 1x1 white, no layers tracked
|
||||
uint alphaTex = gl.GenTexture();
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, alphaTex);
|
||||
gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null);
|
||||
fixed (byte* p = white)
|
||||
gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p);
|
||||
gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
|
||||
return new TerrainAtlas(
|
||||
gl,
|
||||
tex, new Dictionary<uint, uint> { [0] = 0u }, 1,
|
||||
alphaTex, 1,
|
||||
Array.Empty<byte>(), Array.Empty<byte>(), Array.Empty<byte>());
|
||||
}
|
||||
|
||||
public void Dispose() => _gl.DeleteTexture(GlTexture);
|
||||
public void Dispose()
|
||||
{
|
||||
_gl.DeleteTexture(GlTexture);
|
||||
_gl.DeleteTexture(GlAlphaTexture);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ public static class SurfaceDecoder
|
|||
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_INDEX16 when palette is not null => DecodeIndex16(rs, palette, isClipMap),
|
||||
_ => DecodedTexture.Magenta,
|
||||
};
|
||||
|
|
@ -104,6 +105,34 @@ public static class SurfaceDecoder
|
|||
Height: 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode single-byte-per-pixel alpha (PFID_A8 / PFID_CUSTOM_LSCAPE_ALPHA)
|
||||
/// into RGBA8 by replicating each alpha byte into all four channels. AC's
|
||||
/// terrain blending alpha masks are stored as PFID_CUSTOM_LSCAPE_ALPHA and
|
||||
/// other generic 8-bit alpha surfaces use PFID_A8; the bit layout is
|
||||
/// identical so one decoder handles both. Replicating into all four
|
||||
/// channels lets the fragment shader pull "the blend amount" from either
|
||||
/// .a or .r without special-casing.
|
||||
/// </summary>
|
||||
private static DecodedTexture DecodeA8(RenderSurface rs)
|
||||
{
|
||||
int expected = rs.Width * rs.Height;
|
||||
if (rs.SourceData.Length < expected)
|
||||
return DecodedTexture.Magenta;
|
||||
|
||||
var rgba = new byte[expected * 4];
|
||||
for (int i = 0; i < expected; i++)
|
||||
{
|
||||
byte a = rs.SourceData[i];
|
||||
int d = i * 4;
|
||||
rgba[d + 0] = a;
|
||||
rgba[d + 1] = a;
|
||||
rgba[d + 2] = a;
|
||||
rgba[d + 3] = a;
|
||||
}
|
||||
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
||||
}
|
||||
|
||||
private static DecodedTexture DecodeA8R8G8B8(RenderSurface rs)
|
||||
{
|
||||
int expected = rs.Width * rs.Height * 4;
|
||||
|
|
|
|||
|
|
@ -55,6 +55,81 @@ public class SurfaceDecoderTests
|
|||
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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue