From 225f806eab24521f2bbf7da91118b14c2ebf6475 Mon Sep 17 00:00:00 2001
From: Samuliak <samuliak77@gmail.com>
Date: Tue, 2 Jul 2024 15:14:50 +0200
Subject: [PATCH] support etc texture formats

---
 CMakeLists.txt                    |   1 +
 src/core/renderer_mtl/etc1.cpp    | 124 ++++++++++++++++++++++++++++++
 src/core/renderer_mtl/texture.cpp |  10 +--
 3 files changed, 127 insertions(+), 8 deletions(-)
 create mode 100644 src/core/renderer_mtl/etc1.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index c5745d02..a2c704b0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -408,6 +408,7 @@ if(ENABLE_METAL AND APPLE)
     set(RENDERER_MTL_SOURCE_FILES src/core/renderer_mtl/metal_cpp_impl.cpp
         src/core/renderer_mtl/renderer_mtl.cpp
         src/core/renderer_mtl/texture.cpp
+        src/core/renderer_mtl/etc1.cpp
         src/host_shaders/metal_shaders.metal
     )
 
diff --git a/src/core/renderer_mtl/etc1.cpp b/src/core/renderer_mtl/etc1.cpp
new file mode 100644
index 00000000..8018c2dc
--- /dev/null
+++ b/src/core/renderer_mtl/etc1.cpp
@@ -0,0 +1,124 @@
+#include <algorithm>
+#include "colour.hpp"
+#include "renderer_mtl/renderer_mtl.hpp"
+#include "renderer_mtl/texture.hpp"
+
+using namespace Helpers;
+
+namespace Metal {
+
+static constexpr u32 signExtend3To32(u32 val) {
+    return (u32)(s32(val) << 29 >> 29);
+}
+
+u32 Texture::getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, std::span<const u8> data) {
+    // Pixel offset of the 8x8 tile based on u, v and the width of the texture
+    u32 offs = ((u & ~7) * 8) + ((v & ~7) * width);
+    if (!hasAlpha)
+        offs >>= 1;
+
+    // In-tile offsets for u/v
+    u &= 7;
+    v &= 7;
+
+    // ETC1(A4) also subdivide the 8x8 tile to 4 4x4 tiles
+    // Each tile is 8 bytes for ETC1, but since ETC1A4 has 4 alpha bits per pixel, that becomes 16 bytes
+    const u32 subTileSize = hasAlpha ? 16 : 8;
+    const u32 subTileIndex = (u / 4) + 2 * (v / 4); // Which of the 4 subtiles is this texel in?
+
+    // In-subtile offsets for u/v
+    u &= 3;
+    v &= 3;
+    offs += subTileSize * subTileIndex;
+
+    u32 alpha;
+    const u64* ptr = reinterpret_cast<const u64*>(data.data() + offs); // Cast to u64*
+
+    if (hasAlpha) {
+        // First 64 bits of the 4x4 subtile are alpha data
+        const u64 alphaData = *ptr++;
+        alpha = Colour::convert4To8Bit((alphaData >> (4 * (u * 4 + v))) & 0xf);
+    }
+    else {
+        alpha = 0xff; // ETC1 without alpha uses ff for every pixel
+    }
+
+    // Next 64 bits of the subtile are colour data
+    u64 colourData = *ptr;
+    return decodeETC(alpha, u, v, colourData);
+}
+
+u32 Texture::decodeETC(u32 alpha, u32 u, u32 v, u64 colourData) {
+    static constexpr u32 modifiers[8][2] = {
+        { 2, 8 },
+        { 5, 17 },
+        { 9, 29 },
+        { 13, 42 },
+        { 18, 60 },
+        { 24, 80 },
+        { 33, 106 },
+        { 47, 183 },
+    };
+
+    // Parse colour data for 4x4 block
+    const u32 subindices = getBits<0, 16, u32>(colourData);
+    const u32 negationFlags = getBits<16, 16, u32>(colourData);
+    const bool flip = getBit<32>(colourData);
+    const bool diffMode = getBit<33>(colourData);
+
+    // Note: index1 is indeed stored on the higher bits, with index2 in the lower bits
+    const u32 tableIndex1 = getBits<37, 3, u32>(colourData);
+    const u32 tableIndex2 = getBits<34, 3, u32>(colourData);
+    const u32 texelIndex = u * 4 + v; // Index of the texel in the block
+
+    if (flip)
+        std::swap(u, v);
+
+    s32 r, g, b;
+    if (diffMode) {
+        r = getBits<59, 5, s32>(colourData);
+        g = getBits<51, 5, s32>(colourData);
+        b = getBits<43, 5, s32>(colourData);
+
+        if (u >= 2) {
+            r += signExtend3To32(getBits<56, 3, u32>(colourData));
+            g += signExtend3To32(getBits<48, 3, u32>(colourData));
+            b += signExtend3To32(getBits<40, 3, u32>(colourData));
+        }
+
+        // Expand from 5 to 8 bits per channel
+        r = Colour::convert5To8Bit(r);
+        g = Colour::convert5To8Bit(g);
+        b = Colour::convert5To8Bit(b);
+    } else {
+        if (u < 2) {
+            r = getBits<60, 4, s32>(colourData);
+            g = getBits<52, 4, s32>(colourData);
+            b = getBits<44, 4, s32>(colourData);
+        } else {
+            r = getBits<56, 4, s32>(colourData);
+            g = getBits<48, 4, s32>(colourData);
+            b = getBits<40, 4, s32>(colourData);
+        }
+
+        // Expand from 4 to 8 bits per channel
+        r = Colour::convert4To8Bit(r);
+        g = Colour::convert4To8Bit(g);
+        b = Colour::convert4To8Bit(b);
+    }
+
+    const u32 index = (u < 2) ? tableIndex1 : tableIndex2;
+    s32 modifier = modifiers[index][(subindices >> texelIndex) & 1];
+
+    if (((negationFlags >> texelIndex) & 1) != 0) {
+        modifier = -modifier;
+    }
+
+    r = std::clamp(r + modifier, 0, 255);
+    g = std::clamp(g + modifier, 0, 255);
+    b = std::clamp(b + modifier, 0, 255);
+
+    return (alpha << 24) | (u32(b) << 16) | (u32(g) << 8) | u32(r);
+}
+
+} // namespace Metal
diff --git a/src/core/renderer_mtl/texture.cpp b/src/core/renderer_mtl/texture.cpp
index a4c1dd4d..df3866ac 100644
--- a/src/core/renderer_mtl/texture.cpp
+++ b/src/core/renderer_mtl/texture.cpp
@@ -228,14 +228,8 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, std::span<const u8>
             return (alpha << 24) | (intensity << 16) | (intensity << 8) | intensity;
         }
 
-        case PICA::TextureFmt::ETC1: {
-            //return getTexelETC(false, u, v, size.u(), data);
-            Helpers::panic("[Texture::DecodeTexel] Unimplemented format = %d", static_cast<int>(fmt));
-        }
-        case PICA::TextureFmt::ETC1A4: {
-            //return getTexelETC(true, u, v, size.u(), data);
-            Helpers::panic("[Texture::DecodeTexel] Unimplemented format = %d", static_cast<int>(fmt));
-        }
+        case PICA::TextureFmt::ETC1: return getTexelETC(false, u, v, size.u(), data);
+        case PICA::TextureFmt::ETC1A4: return getTexelETC(true, u, v, size.u(), data);
 
         default:
             Helpers::panic("[Texture::DecodeTexel] Unimplemented format = %d", static_cast<int>(fmt));