summaryrefslogtreecommitdiff
path: root/src/factories/pm64/MapTextureFactory.cpp
blob: ff652bca8132645999d91bea44f85dc379e8f373 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
#include "MapTextureFactory.h"
#include "Companion.h"
#include "utils/Decompressor.h"
#include "spdlog/spdlog.h"

// PM64 TextureHeader structure (0x30 bytes):
//   0x00: name[32] - texture name string
//   0x20: auxW (u16)
//   0x22: mainW (u16)
//   0x24: auxH (u16)
//   0x26: mainH (u16)
//   0x28: isVariant (u8)
//   0x29: extraTiles (u8)
//   0x2A: auxCombineType:6, auxCombineSubType:2 (u8)
//   0x2B: auxFmt:4, mainFmt:4 (u8)
//   0x2C: auxBitDepth:4, mainBitDepth:4 (u8)
//   0x2D: auxWrapW:4, mainWrapW:4 (u8)
//   0x2E: auxWrapH:4, mainWrapH:4 (u8)
//   0x2F: filtering (u8)

static constexpr size_t TEXTURE_HEADER_SIZE = 0x30;

// Image format/bit depth constants (matching N64 GBI)
enum ImgFmt {
    G_IM_FMT_RGBA = 0,
    G_IM_FMT_YUV = 1,
    G_IM_FMT_CI = 2,
    G_IM_FMT_IA = 3,
    G_IM_FMT_I = 4,
};

enum ImgSiz {
    G_IM_SIZ_4b = 0,
    G_IM_SIZ_8b = 1,
    G_IM_SIZ_16b = 2,
    G_IM_SIZ_32b = 3,
};

enum ExtraTiles {
    EXTRA_TILE_NONE = 0,
    EXTRA_TILE_MIPMAPS = 1,
    EXTRA_TILE_AUX_SAME_AS_MAIN = 2,
    EXTRA_TILE_AUX_INDEPENDENT = 3,
};

static void ByteSwapTextureHeader(uint8_t* headerPtr) {
    // Byte-swap u16 fields at offsets 0x20, 0x22, 0x24, 0x26
    uint16_t* auxW = reinterpret_cast<uint16_t*>(headerPtr + 0x20);
    uint16_t* mainW = reinterpret_cast<uint16_t*>(headerPtr + 0x22);
    uint16_t* auxH = reinterpret_cast<uint16_t*>(headerPtr + 0x24);
    uint16_t* mainH = reinterpret_cast<uint16_t*>(headerPtr + 0x26);

    *auxW = BSWAP16(*auxW);
    *mainW = BSWAP16(*mainW);
    *auxH = BSWAP16(*auxH);
    *mainH = BSWAP16(*mainH);

    // Fix bitfield byte layout for little-endian.
    // On N64 (big-endian), GCC lays out the first-declared bitfield in the UPPER bits.
    // On LE (ARM64/x86), the first-declared bitfield occupies the LOWER bits.
    // The TextureHeader struct has paired bitfields within single bytes:
    //   0x2A: auxCombineType:6, auxCombineSubType:2
    //   0x2B: auxFmt:4, mainFmt:4
    //   0x2C: auxBitDepth:4, mainBitDepth:4
    //   0x2D: auxWrapW:4, mainWrapW:4
    //   0x2E: auxWrapH:4, mainWrapH:4
    // Without rearranging, the LE struct reads mainFmt where auxFmt should be (and vice versa).

    // 0x2A: 6:2 split — N64 byte = (combineType << 2) | combineSubType
    // LE needs: combineType | (combineSubType << 6)
    uint8_t b = headerPtr[0x2A];
    headerPtr[0x2A] = ((b >> 2) & 0x3F) | ((b & 0x03) << 6);

    // 0x2B-0x2E: 4:4 splits — swap nibbles
    for (int i = 0x2B; i <= 0x2E; i++) {
        b = headerPtr[i];
        headerPtr[i] = ((b & 0x0F) << 4) | ((b >> 4) & 0x0F);
    }
}

// Calculate raster size for a texture (including mipmaps if present)
static uint32_t CalculateRasterSize(uint16_t width, uint16_t height, uint8_t bitDepth, uint8_t extraTiles) {
    uint32_t rasterSize = width * height;

    // Compute mipmaps size if present
    if (extraTiles == EXTRA_TILE_MIPMAPS) {
        if (bitDepth == G_IM_SIZ_4b) {
            int d = 2;
            while (width / d >= 16 && height / d > 0) {
                rasterSize += (width / d) * (height / d);
                d *= 2;
            }
        } else if (bitDepth == G_IM_SIZ_8b) {
            int d = 2;
            while (width / d >= 8 && height / d > 0) {
                rasterSize += (width / d) * (height / d);
                d *= 2;
            }
        } else if (bitDepth == G_IM_SIZ_16b) {
            int d = 2;
            while (width / d >= 4 && height / d > 0) {
                rasterSize += (width / d) * (height / d);
                d *= 2;
            }
        } else if (bitDepth == G_IM_SIZ_32b) {
            int d = 2;
            while (width / d >= 2 && height / d > 0) {
                rasterSize += (width / d) * (height / d);
                d *= 2;
            }
        }
    }

    // Scale by bit depth
    if (bitDepth == G_IM_SIZ_4b) {
        rasterSize /= 2;
    } else if (bitDepth == G_IM_SIZ_16b) {
        rasterSize *= 2;
    } else if (bitDepth == G_IM_SIZ_32b) {
        rasterSize *= 4;
    }

    return rasterSize;
}

// Calculate palette size for a texture
static uint32_t CalculatePaletteSize(uint8_t fmt, uint8_t bitDepth) {
    if (fmt == G_IM_FMT_CI) {
        return (bitDepth == G_IM_SIZ_8b) ? 0x200 : 0x20;
    }
    return 0;
}

static void ByteSwapAllTextureHeaders(uint8_t* data, size_t size) {
    size_t offset = 0;

    while (offset + TEXTURE_HEADER_SIZE <= size) {
        uint8_t* headerPtr = data + offset;

        // Check if this looks like a valid texture header (name should be ASCII)
        bool validName = true;
        for (int i = 0; i < 32 && headerPtr[i] != 0; i++) {
            if (headerPtr[i] < 0x20 || headerPtr[i] > 0x7E) {
                validName = false;
                break;
            }
        }

        if (!validName) {
            // End of texture list or invalid data
            break;
        }

        // Read header fields (still in big-endian at this point)
        uint16_t mainW = (headerPtr[0x22] << 8) | headerPtr[0x23];
        uint16_t mainH = (headerPtr[0x26] << 8) | headerPtr[0x27];
        uint16_t auxW = (headerPtr[0x20] << 8) | headerPtr[0x21];
        uint16_t auxH = (headerPtr[0x24] << 8) | headerPtr[0x25];
        uint8_t extraTiles = headerPtr[0x29];
        uint8_t mainBitDepth = headerPtr[0x2C] & 0x0F;
        uint8_t mainFmt = headerPtr[0x2B] & 0x0F;
        uint8_t auxBitDepth = (headerPtr[0x2C] >> 4) & 0x0F;
        uint8_t auxFmt = (headerPtr[0x2B] >> 4) & 0x0F;

        // Validate dimensions
        if (mainW == 0 || mainH == 0 || mainW > 1024 || mainH > 1024) {
            break;
        }

        // Byte-swap this header
        ByteSwapTextureHeader(headerPtr);

        // Calculate texture data size to skip to next header
        uint32_t rasterSize = CalculateRasterSize(mainW, mainH, mainBitDepth, extraTiles);
        uint32_t paletteSize = CalculatePaletteSize(mainFmt, mainBitDepth);

        uint32_t auxRasterSize = 0;
        uint32_t auxPaletteSize = 0;
        if (extraTiles == EXTRA_TILE_AUX_INDEPENDENT) {
            auxRasterSize = CalculateRasterSize(auxW, auxH, auxBitDepth, EXTRA_TILE_NONE);
            auxPaletteSize = CalculatePaletteSize(auxFmt, auxBitDepth);
        }

        // Move to next texture entry
        offset += TEXTURE_HEADER_SIZE + rasterSize + paletteSize + auxRasterSize + auxPaletteSize;
    }

    SPDLOG_DEBUG("Byte-swapped texture headers up to offset 0x{:X}", offset);
}

std::optional<std::shared_ptr<IParsedData>> PM64MapTextureFactory::parse(std::vector<uint8_t>& buffer,
                                                                         YAML::Node& node) {
    auto offset = GetSafeNode<uint32_t>(node, "offset");

    // Check if compressed (YAY0)
    auto compressionType = Decompressor::GetCompressionType(buffer, offset);

    std::vector<uint8_t> textureData;

    if (compressionType == CompressionType::YAY0) {
        auto decoded = Decompressor::Decode(buffer, offset, CompressionType::YAY0);
        if (!decoded || decoded->size == 0) {
            SPDLOG_ERROR("Failed to decompress YAY0 map texture data at offset 0x{:X}", offset);
            return std::nullopt;
        }

        textureData.assign(decoded->data, decoded->data + decoded->size);
    } else {
        // Uncompressed - read raw data
        // For uncompressed textures, we need to determine the size from somewhere
        // Usually specified in YAML or we read until we hit invalid data
        auto sizeOpt = GetSafeNode<size_t>(node, "size", 0);
        size_t size = sizeOpt;

        if (size == 0) {
            // Try to auto-detect size by scanning for valid texture headers
            // This is a fallback - normally size should be in YAML
            size = 0x40000; // Max reasonable size
        }

        if (offset + size > buffer.size()) {
            size = buffer.size() - offset;
        }

        textureData.assign(buffer.begin() + offset, buffer.begin() + offset + size);
    }

    // Byte-swap all texture headers in the data
    ByteSwapAllTextureHeaders(textureData.data(), textureData.size());

    return std::make_shared<RawBuffer>(textureData);
}

ExportResult PM64MapTextureBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
                                                  std::string& entryName, YAML::Node& node, std::string* replacement) {
    auto writer = LUS::BinaryWriter();
    auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;

    // Write as Blob type - game loads as raw binary
    WriteHeader(writer, Torch::ResourceType::Blob, 0);
    writer.Write(static_cast<uint32_t>(data.size()));
    writer.Write(reinterpret_cast<char*>(data.data()), data.size());
    writer.Finish(write);

    return std::nullopt;
}

ExportResult PM64MapTextureHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
                                                  std::string& entryName, YAML::Node& node, std::string* replacement) {
    const auto symbol = GetSafeNode(node, "symbol", entryName);

    if (Companion::Instance->IsOTRMode()) {
        write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
        return std::nullopt;
    }

    write << "extern u8 " << symbol << "[];\n";
    return std::nullopt;
}