yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
lyra_layout_descriptor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cstddef>
6#include <cstdint>
7#include <cstring>
8#include <string>
9#include <utility>
10#include <vector>
11
12namespace yaze::rom {
13namespace {
14
15constexpr size_t kDescriptorPc = 0x147000;
16constexpr size_t kDescriptorSize = 1024;
17constexpr size_t kHeaderSize = 32;
18constexpr size_t kEntrySize = 16;
19constexpr size_t kEntryCount = 35;
21constexpr char kMagic[] = "OOSOW192";
22
23struct EntrySpec {
24 uint16_t id;
25 uint16_t kind;
26 uint16_t count;
27 uint16_t stride;
28};
29
30// Revision-1's directory order, domains and widths. Addresses are read from
31// the descriptor and then checked against the audited allocation windows.
32constexpr std::array<EntrySpec, kEntryCount> kSpecs = {{
33 {1, 1, 192, 2}, {2, 1, 192, 1}, {3, 1, 192, 1},
34 {4, 1, 192, 1}, {5, 1, 192, 2}, {6, 1, 192, 8},
35 {7, 1, 192, 1}, {8, 1, 192, 2}, {9, 1, 192, 2},
36 {10, 1, 192, 2}, {11, 1, 192, 2}, {12, 1, 192, 2},
37 {13, 1, 192, 2}, {14, 1, 192, 2}, {15, 1, 192, 2},
38 {16, 1, 192, 2}, {17, 1, 192, 2}, {18, 1, 192, 2},
39 {19, 1, 192, 2}, {20, 1, 192, 3}, {21, 1, 192, 3},
40 {22, 1, 192, 2}, {23, 1, 192, 2}, {24, 1, 64, 1},
41 {25, 1, 64, 1}, {26, 1, 192, 1}, {27, 1, 64, 1},
42 {28, 1, 128, 1}, {29, 1, 192, 1}, {30, 1, 192, 1},
43 {31, 1, 192, 1}, {100, 2, 16384, 1}, {200, 3, 32368, 1},
44 {201, 3, 16671, 1}, {202, 3, 32768, 1},
45}};
46
47constexpr std::array<std::pair<uint32_t, uint32_t>, 3> kStreamWindows = {{
48 {0x58000, 0x5FE70},
49 {0x60000, 0x6411F},
50 {0x130000, 0x138000},
51}};
52
53uint16_t U16(const uint8_t* p) {
54 return static_cast<uint16_t>(p[0] | (p[1] << 8));
55}
56
57uint32_t U24(const uint8_t* p) {
58 return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8) |
59 (static_cast<uint32_t>(p[2]) << 16);
60}
61
62uint32_t U32(const uint8_t* p) {
63 return U24(p) | (static_cast<uint32_t>(p[3]) << 24);
64}
65
66uint32_t Crc32(const uint8_t* p, size_t length) {
67 uint32_t crc = 0xFFFFFFFF;
68 for (size_t i = 0; i < length; ++i) {
69 crc ^= p[i];
70 for (int bit = 0; bit < 8; ++bit) {
71 crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320U : 0U);
72 }
73 }
74 return crc ^ 0xFFFFFFFF;
75}
76
77bool ToPc(uint32_t snes, uint32_t* pc) {
78 const uint32_t bank = snes >> 16;
79 if (bank > 0x3F || (snes & 0x8000) == 0) {
80 return false;
81 }
82 *pc = bank * 0x8000 + (snes & 0x7FFF);
83 return true;
84}
85
86LyraLayoutInspection Invalid(std::string error, uint16_t revision = 0,
87 uint16_t capacity = 0, uint16_t active = 0) {
90 result.revision = revision;
91 result.capacity = capacity;
92 result.active_count = active;
93 result.write_supported = false;
94 result.error = std::move(error);
95 return result;
96}
97
98bool InStreamWindow(uint32_t pc) {
99 for (const auto& [begin, end] : kStreamWindows) {
100 if (pc >= begin && pc < end) {
101 return true;
102 }
103 }
104 return false;
105}
106
107bool StreamPointerPc(size_t size, uint32_t snes, uint32_t* pc) {
108 // Existing stream tables use $8B/$8C/$A6 cartridge mirrors. Only bit 23
109 // is normalized; every result must still land in a declared stream window.
110 return ToPc(snes & 0x7FFFFF, pc) && *pc < size && InStreamWindow(*pc);
111}
112
113bool DecodeStream(const uint8_t* image, size_t size, uint32_t start,
114 std::array<uint8_t, 256>* decoded) {
115 size_t limit = std::min<size_t>(size, ((start / 0x8000) + 1) * 0x8000);
116 for (const auto& [begin, end] : kStreamWindows) {
117 if (start >= begin && start < end) {
118 limit = std::min<size_t>(limit, end);
119 break;
120 }
121 }
122 size_t p = start;
123 size_t used = 0;
124 auto take = [&](uint8_t* byte) {
125 if (p >= limit)
126 return false;
127 *byte = image[p++];
128 return true;
129 };
130 while (p < limit) {
131 uint8_t command = 0;
132 if (!take(&command))
133 return false;
134 if (command == 0xFF)
135 return used == decoded->size();
136 unsigned kind = 0;
137 unsigned count = 0;
138 if (command >= 0xE0) {
139 uint8_t low = 0;
140 if (!take(&low))
141 return false;
142 kind = (command >> 2) & 7;
143 count = ((command & 3) << 8) + low + 1;
144 } else {
145 kind = command >> 5;
146 count = (command & 31) + 1;
147 }
148 if (used + count > decoded->size())
149 return false;
150 uint8_t a = 0;
151 uint8_t b = 0;
152 if (kind == 0) {
153 for (unsigned i = 0; i < count; ++i) {
154 if (!take(&(*decoded)[used++]))
155 return false;
156 }
157 } else if (kind == 1) {
158 if (!take(&a))
159 return false;
160 std::fill_n(decoded->begin() + used, count, a);
161 used += count;
162 } else if (kind == 2) {
163 if (!take(&a) || !take(&b))
164 return false;
165 for (unsigned i = 0; i < count; ++i) {
166 (*decoded)[used++] = i % 2 ? b : a;
167 }
168 } else if (kind == 3) {
169 if (!take(&a))
170 return false;
171 for (unsigned i = 0; i < count; ++i) {
172 (*decoded)[used++] = static_cast<uint8_t>(a + i);
173 }
174 } else {
175 if (!take(&a) || !take(&b))
176 return false;
177 const unsigned offset = (a << 8) | b;
178 for (unsigned i = 0; i < count; ++i) {
179 if (offset + i >= used)
180 return false;
181 (*decoded)[used++] = (*decoded)[offset + i];
182 }
183 }
184 }
185 return false;
186}
187
188bool CheckSpritePointer(const uint8_t* image, size_t size, uint16_t word) {
189 if (word < 0xC000) {
190 return false;
191 }
192 size_t pc = 0x1E0000 + (word & 0x7FFF);
193 for (int record = 0; record <= 128; ++record) {
194 if (pc >= 0x1E8000 || pc >= size) {
195 return false;
196 }
197 if (image[pc] == 0xFF) {
198 return true;
199 }
200 if (record == 128 || pc + 3 > 0x1E8000) {
201 return false;
202 }
203 pc += 3;
204 }
205 return false;
206}
207
208} // namespace
209
210LyraLayoutInspection InspectLyraLayout(const uint8_t* image, size_t size) {
212 if (image == nullptr || size <= kDescriptorPc) {
213 return absent;
214 }
215 const uint8_t* d = image + kDescriptorPc;
216 const size_t available = std::min(kDescriptorSize, size - kDescriptorPc);
217 const bool signature_prefix =
218 available >= 5 && std::memcmp(d, "OOSOW", 5) == 0;
219 // A damaged magic still belongs to this format when its independent header
220 // and directory fingerprint survive. Nonblank bytes alone mean nothing.
221 const bool directory_fingerprint =
222 available >= kUsedSize && U16(d + 0x0A) == kHeaderSize &&
223 U16(d + 0x0C) == kUsedSize && U16(d + 0x0E) == 192 &&
224 U16(d + 0x10) == 160 && U16(d + 0x12) == kEntryCount &&
225 U16(d + kHeaderSize) == 1 && U16(d + kHeaderSize + 2) == 1;
226 if (!signature_prefix && !directory_fingerprint) {
227 return absent;
228 }
229 if (available != kDescriptorSize) {
230 return Invalid("Truncated Lyra descriptor region");
231 }
232 if (std::memcmp(d, kMagic, 8) != 0) {
233 return Invalid("Unknown Lyra descriptor magic");
234 }
235
236 const uint16_t revision = U16(d + 8);
237 const uint16_t capacity = U16(d + 0x0E);
238 const uint16_t active = U16(d + 0x10);
239 auto bad = [&](std::string error) {
240 return Invalid(std::move(error), revision, capacity, active);
241 };
242 if (revision != 1)
243 return bad("Unsupported Lyra ROM-format revision");
244 if (U16(d + 0x0A) != kHeaderSize || U16(d + 0x0C) != kUsedSize ||
245 U16(d + 0x12) != kEntryCount) {
246 return bad("Invalid Lyra descriptor length or directory count");
247 }
248 if (capacity != 192 || active != 160) {
249 return bad("Invalid Lyra capacity or active count");
250 }
251 if (d[0x14] != 0x3C || d[0x15] != 1 || U16(d + 0x16) != 384) {
252 return bad("Unsupported Lyra sprite bank, flags or phase stride");
253 }
254 if (U32(d + 0x18) != size) {
255 return bad("Lyra image length mismatch");
256 }
257 if (image[0x140145] != 3 || image[0x1423FF] == 0xEA) {
258 return bad("Conflicting ZS version or older $EA tail identity");
259 }
260 std::array<uint8_t, kDescriptorSize> checked;
261 std::copy_n(d, kDescriptorSize, checked.begin());
262 std::fill_n(checked.begin() + 0x1C, 4, 0);
263 if (Crc32(checked.data(), checked.size()) != U32(d + 0x1C)) {
264 return bad("Lyra descriptor CRC mismatch");
265 }
266 if (std::any_of(d + kUsedSize, d + kDescriptorSize,
267 [](uint8_t b) { return b != 0; })) {
268 return bad("Nonzero Lyra descriptor padding");
269 }
270
273 result.revision = revision;
274 result.capacity = capacity;
275 result.active_count = active;
276 result.write_supported = false;
277 result.entries.reserve(kEntryCount);
278 for (size_t i = 0; i < kEntryCount; ++i) {
279 const uint8_t* p = d + kHeaderSize + i * kEntrySize;
280 const auto& spec = kSpecs[i];
281 LyraLayoutEntry e{U16(p), U16(p + 2), U32(p + 4), 0,
282 U16(p + 8), U16(p + 10), U32(p + 12)};
283 if (e.id != spec.id || e.kind != spec.kind || e.count != spec.count ||
284 e.stride != spec.stride) {
285 return bad("Invalid Lyra directory entry");
286 }
287 if (!ToPc(e.snes_address, &e.pc_address)) {
288 return bad("Invalid Lyra directory bank or address");
289 }
290 const uint32_t length = static_cast<uint32_t>(e.count) * e.stride;
291 if ((e.snes_address & 0xFFFF) + length > 0x10000) {
292 return bad("Lyra table crosses bank");
293 }
294 if (e.pc_address > size || length > size - e.pc_address) {
295 return bad("Lyra directory outside image extent");
296 }
297 if (i < 31) {
298 if (i == 29 && e.snes_address != 0x02F88D) {
299 return bad("Invalid Lyra area-size table address");
300 }
301 if (i == 30 && e.snes_address != 0x09C635) {
302 return bad("Invalid Lyra palette-set table address");
303 }
304 if (i < 29 &&
305 (e.pc_address < 0x144000 || e.pc_address + length > 0x146840)) {
306 return bad("Lyra table outside owned bank-28 reservation");
307 }
308 if (i < 19 && (e.snes_address >> 16) != 0x28) {
309 return bad("Lyra Pool table bank mismatch");
310 }
311 if (i > 0 && i < 19 &&
312 result.entries.back().pc_address +
313 result.entries.back().count * result.entries.back().stride !=
314 e.pc_address) {
315 return bad("Lyra Pool table overlap or gap");
316 }
317 } else if (i == 31) {
318 if (e.pc_address != 0x1E4000) {
319 return bad("Lyra sprite list region changed");
320 }
321 } else if (e.pc_address != kStreamWindows[i - 32].first) {
322 return bad("Lyra stream region changed");
323 }
324 for (const auto& prior : result.entries) {
325 const uint32_t prior_end = prior.pc_address + prior.count * prior.stride;
326 if (e.pc_address < prior_end &&
327 prior.pc_address < e.pc_address + length) {
328 return bad("Lyra directory overlap");
329 }
330 }
331 result.entries.push_back(e);
332 }
333 if (result.entries[16].pc_address - result.entries[15].pc_address != 384 ||
334 result.entries[17].pc_address - result.entries[16].pc_address != 384) {
335 return bad("Lyra sprite phase stride mismatch");
336 }
337 for (const auto& e : result.entries) {
338 const size_t length = static_cast<size_t>(e.count) * e.stride;
339 if (Crc32(image + e.pc_address, length) != e.crc32) {
340 return bad("Lyra data CRC mismatch");
341 }
342 }
343 for (size_t map = 0; map < 192; ++map) {
344 std::array<uint8_t, 256> high{};
345 std::array<uint8_t, 256> low{};
346 for (size_t table : {19U, 20U}) {
347 const auto& e = result.entries[table];
348 uint32_t stream_pc = 0;
349 if (!StreamPointerPc(size, U24(image + e.pc_address + map * 3),
350 &stream_pc)) {
351 return bad("Lyra map pointer outside owned stream regions");
352 }
353 if (!DecodeStream(image, size, stream_pc, table == 19 ? &high : &low)) {
354 return bad("Lyra compressed map stream invalid");
355 }
356 }
357 for (size_t tile = 0; tile < high.size(); ++tile) {
358 if ((static_cast<uint16_t>(high[tile]) << 8 | low[tile]) >= 17728) {
359 return bad("Lyra Tile32 reference exceeds existing capacity");
360 }
361 }
362 for (size_t table : {15U, 16U, 17U}) {
363 const auto& e = result.entries[table];
364 if (!CheckSpritePointer(image, size,
365 U16(image + e.pc_address + map * 2))) {
366 return bad("Lyra sprite pointer outside list region or unterminated");
367 }
368 }
369 if (map >= active) {
370 const auto& parent = result.entries[6];
371 const auto& role = result.entries[28];
372 const auto& overlay = result.entries[4];
373 const auto& area_size = result.entries[29];
374 if (image[parent.pc_address + map] != map ||
375 image[role.pc_address + map] != 3 ||
376 U16(image + overlay.pc_address + map * 2) != 0x00FF ||
377 image[area_size.pc_address + map] != 0) {
378 return bad("Lyra inactive tail role or parent is invalid");
379 }
380 }
381 }
382 return result;
383}
384
385std::optional<std::span<const uint8_t>> LyraLayoutRecord(
386 const LyraLayoutInspection& layout, const uint8_t* image, size_t size,
387 uint16_t table_id, size_t index) {
388 if (layout.presence != LyraLayoutPresence::kValid || image == nullptr) {
389 return std::nullopt;
390 }
391 for (const auto& entry : layout.entries) {
392 if (entry.id != table_id) {
393 continue;
394 }
395 if (entry.kind != 1 || index >= entry.count) {
396 return std::nullopt;
397 }
398 const size_t start = entry.pc_address + index * entry.stride;
399 if (start > size || entry.stride > size - start) {
400 return std::nullopt;
401 }
402 return std::span<const uint8_t>(image + start, entry.stride);
403 }
404 return std::nullopt;
405}
406
407std::optional<std::array<uint16_t, 256>> LyraMapTile32(
408 const LyraLayoutInspection& layout, const uint8_t* image, size_t size,
409 size_t map_index) {
410 auto high_record = LyraLayoutRecord(layout, image, size, 20, map_index);
411 auto low_record = LyraLayoutRecord(layout, image, size, 21, map_index);
412 if (!high_record || !low_record || high_record->size() != 3 ||
413 low_record->size() != 3) {
414 return std::nullopt;
415 }
416 std::array<uint8_t, 256> high{};
417 std::array<uint8_t, 256> low{};
418 uint32_t high_pc = 0;
419 uint32_t low_pc = 0;
420 if (!StreamPointerPc(size, U24(high_record->data()), &high_pc) ||
421 !StreamPointerPc(size, U24(low_record->data()), &low_pc) ||
422 !DecodeStream(image, size, high_pc, &high) ||
423 !DecodeStream(image, size, low_pc, &low)) {
424 return std::nullopt;
425 }
426 std::array<uint16_t, 256> tiles{};
427 for (size_t i = 0; i < tiles.size(); ++i) {
428 tiles[i] = static_cast<uint16_t>((high[i] << 8) | low[i]);
429 if (tiles[i] >= 17728) {
430 return std::nullopt;
431 }
432 }
433 return tiles;
434}
435
436} // namespace yaze::rom
bool StreamPointerPc(size_t size, uint32_t snes, uint32_t *pc)
bool CheckSpritePointer(const uint8_t *image, size_t size, uint16_t word)
constexpr std::array< std::pair< uint32_t, uint32_t >, 3 > kStreamWindows
constexpr std::array< EntrySpec, kEntryCount > kSpecs
bool DecodeStream(const uint8_t *image, size_t size, uint32_t start, std::array< uint8_t, 256 > *decoded)
std::optional< std::span< const uint8_t > > LyraLayoutRecord(const LyraLayoutInspection &layout, const uint8_t *image, size_t size, uint16_t table_id, size_t index)
LyraLayoutInspection InspectLyraLayout(const uint8_t *image, size_t size)
std::optional< std::array< uint16_t, 256 > > LyraMapTile32(const LyraLayoutInspection &layout, const uint8_t *image, size_t size, size_t map_index)
std::vector< LyraLayoutEntry > entries