From 0a67254c5e3f463750c34ec5e0d796c745ea6ed3 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 8 May 2026 11:32:37 +0200 Subject: [PATCH] refactor(N.3): thread isAdditive + substitute 5 decode methods with WB TextureHelpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 2 — isAdditive threading: SurfaceDecoder.DecodeRenderSurface now accepts isAdditive parameter. A8/CUSTOM_LSCAPE_ALPHA format splits: - isAdditive=true: R=G=B=A=val (terrain alpha, additive entity textures) - isAdditive=false: R=G=B=255, A=val (non-additive entity textures) TextureCache passes surface.Type.HasFlag(SurfaceType.Additive). TerrainAtlas passes isAdditive:true (alpha masks always replicate). Aligns with WB ObjectMeshManager dispatch logic. Task 3 — WB body substitution + new formats: INDEX16, P8, A8R8G8B8, R8G8B8, A8 now delegate to TextureHelpers.FillIndex16/FillP8/FillA8R8G8B8/FillR8G8B8/ FillA8/FillA8Additive. Validation + DecodedTexture wrapping stays ours. X8R8G8B8, DXT1/3/5, SolidColor remain our implementations (no WB equiv). Bonus: R5G6B5 + A4R4G4B4 formats now handled (previously fell to magenta). 9 conformance tests pass. Build 0 errors. Co-Authored-By: Claude Opus 4.6 --- src/AcDream.App/Rendering/TerrainAtlas.cs | 8 +- src/AcDream.App/Rendering/TextureCache.cs | 3 +- src/AcDream.Core/Textures/SurfaceDecoder.cs | 133 +++++++------------- 3 files changed, 50 insertions(+), 94 deletions(-) diff --git a/src/AcDream.App/Rendering/TerrainAtlas.cs b/src/AcDream.App/Rendering/TerrainAtlas.cs index 6e8584a..faa3a6e 100644 --- a/src/AcDream.App/Rendering/TerrainAtlas.cs +++ b/src/AcDream.App/Rendering/TerrainAtlas.cs @@ -316,10 +316,10 @@ public sealed unsafe class TerrainAtlas : IDisposable 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); + // format) or the more generic PFID_A8; terrain blending alpha masks + // MUST use isAdditive=true so R=G=B=A=val — the terrain fragment shader + // reads .r for the blend weight. Palette is not used. + var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true); if (ReferenceEquals(d, DecodedTexture.Magenta)) return false; diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs index 077a12c..b5585c3 100644 --- a/src/AcDream.App/Rendering/TextureCache.cs +++ b/src/AcDream.App/Rendering/TextureCache.cs @@ -199,8 +199,9 @@ public sealed unsafe class TextureCache : IDisposable // Clipmap surfaces use palette indices 0..7 as transparent sentinels. bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap); + bool isAdditive = surface.Type.HasFlag(SurfaceType.Additive); - return SurfaceDecoder.DecodeRenderSurface(rs, effectivePalette, isClipMap); + return SurfaceDecoder.DecodeRenderSurface(rs, effectivePalette, isClipMap, isAdditive); } /// diff --git a/src/AcDream.Core/Textures/SurfaceDecoder.cs b/src/AcDream.Core/Textures/SurfaceDecoder.cs index e48b9a4..8b3158f 100644 --- a/src/AcDream.Core/Textures/SurfaceDecoder.cs +++ b/src/AcDream.Core/Textures/SurfaceDecoder.cs @@ -1,5 +1,6 @@ using BCnEncoder.Decoder; using BCnEncoder.Shared; +using Chorizite.OpenGLSDLBackend.Lib; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; @@ -16,7 +17,7 @@ public static class SurfaceDecoder /// when a palette is available. /// public static DecodedTexture DecodeRenderSurface(RenderSurface rs) - => DecodeRenderSurface(rs, palette: null); + => DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: false); /// /// Decode a RenderSurface's pixel bytes into RGBA8 with optional palette support. @@ -24,8 +25,11 @@ public static class SurfaceDecoder /// 16-bit value in SourceData is treated as an index into . /// When is true on an indexed surface, palette indices /// below 8 are forced to fully-transparent (AC's clipmap alpha-key convention). + /// When is true, A8/CUSTOM_LSCAPE_ALPHA surfaces + /// replicate the byte into all four channels (R=G=B=A=val, for terrain alpha masks + /// and additive surfaces). When false, R=G=B=255, A=val (WB FillA8 semantics). /// - public static DecodedTexture DecodeRenderSurface(RenderSurface rs, Palette? palette, bool isClipMap = false) + public static DecodedTexture DecodeRenderSurface(RenderSurface rs, Palette? palette, bool isClipMap = false, bool isAdditive = false) { if (rs.SourceData is null || rs.Width <= 0 || rs.Height <= 0) return DecodedTexture.Magenta; @@ -40,9 +44,11 @@ public static class SurfaceDecoder PixelFormat.PFID_DXT1 => DecodeBc(rs, CompressionFormat.Bc1, isClipMap), PixelFormat.PFID_DXT3 => DecodeBc(rs, CompressionFormat.Bc2, isClipMap), PixelFormat.PFID_DXT5 => DecodeBc(rs, CompressionFormat.Bc3, isClipMap), - PixelFormat.PFID_A8 or PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA => DecodeA8(rs), + PixelFormat.PFID_A8 or PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA => DecodeA8(rs, isAdditive), PixelFormat.PFID_P8 when palette is not null => DecodeP8(rs, palette, isClipMap), PixelFormat.PFID_INDEX16 when palette is not null => DecodeIndex16(rs, palette, isClipMap), + PixelFormat.PFID_R5G6B5 => DecodeR5G6B5(rs), + PixelFormat.PFID_A4R4G4B4 => DecodeA4R4G4B4(rs), _ => DecodedTexture.Magenta, }; } @@ -59,33 +65,7 @@ public static class SurfaceDecoder 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++) - { - // Read each 16-bit value little-endian as a palette index - int src = i * 2; - ushort idx = (ushort)(rs.SourceData[src] | (rs.SourceData[src + 1] << 8)); - if (idx > paletteMax) idx = 0; - var c = palette.Colors[idx]; - - int dst = i * 4; - // Clipmap alpha-key convention (ACViewer: if (isClipMap && color < 8) r=g=b=a=0): - // palette indices 0..7 on clipmap surfaces represent transparent pixels. - 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; - } - } + TextureHelpers.FillIndex16(rs.SourceData, palette, rgba.AsSpan(), rs.Width, rs.Height, isClipMap); return new DecodedTexture(rgba, rs.Width, rs.Height); } @@ -109,30 +89,22 @@ public static class SurfaceDecoder } /// - /// 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. + /// Decode single-byte-per-pixel alpha (PFID_A8 / PFID_CUSTOM_LSCAPE_ALPHA) into RGBA8. + /// When is true: R=G=B=A=val (terrain alpha masks and + /// additive entity textures — the shader reads .r for the blend weight). When false: + /// R=G=B=255, A=val (WB FillA8 semantics for non-additive entity textures). /// - private static DecodedTexture DecodeA8(RenderSurface rs) + private static DecodedTexture DecodeA8(RenderSurface rs, bool isAdditive) { 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; - } + if (isAdditive) + TextureHelpers.FillA8Additive(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height); + else + TextureHelpers.FillA8(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height); return new DecodedTexture(rgba, rs.Width, rs.Height); } @@ -143,15 +115,7 @@ public static class SurfaceDecoder 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 - } + TextureHelpers.FillA8R8G8B8(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height); return new DecodedTexture(rgba, rs.Width, rs.Height); } @@ -168,29 +132,7 @@ public static class SurfaceDecoder 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; - } - } + TextureHelpers.FillP8(rs.SourceData, palette, rgba.AsSpan(), rs.Width, rs.Height, isClipMap); return new DecodedTexture(rgba, rs.Width, rs.Height); } @@ -207,16 +149,7 @@ public static class SurfaceDecoder 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 - } + TextureHelpers.FillR8G8B8(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height); return new DecodedTexture(rgba, rs.Width, rs.Height); } @@ -245,6 +178,28 @@ public static class SurfaceDecoder return new DecodedTexture(rgba, rs.Width, rs.Height); } + private static DecodedTexture DecodeR5G6B5(RenderSurface rs) + { + int expectedBytes = rs.Width * rs.Height * 2; + if (rs.SourceData.Length < expectedBytes) + return DecodedTexture.Magenta; + + var rgba = new byte[rs.Width * rs.Height * 4]; + TextureHelpers.FillR5G6B5(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height); + return new DecodedTexture(rgba, rs.Width, rs.Height); + } + + private static DecodedTexture DecodeA4R4G4B4(RenderSurface rs) + { + int expectedBytes = rs.Width * rs.Height * 2; + if (rs.SourceData.Length < expectedBytes) + return DecodedTexture.Magenta; + + var rgba = new byte[rs.Width * rs.Height * 4]; + TextureHelpers.FillA4R4G4B4(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height); + return new DecodedTexture(rgba, rs.Width, rs.Height); + } + private static DecodedTexture DecodeBc(RenderSurface rs, CompressionFormat format, bool isClipMap) { var pixels = BcDecoder.DecodeRaw(rs.SourceData, rs.Width, rs.Height, format);