yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
gfx_sheet_storage.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <utility>
5
6#include "absl/strings/str_format.h"
7#include "absl/strings/str_join.h"
9#include "rom/snes.h"
10#include "util/log.h"
11#include "util/macro.h"
12
13namespace yaze::zelda3 {
14
16 constexpr size_t kRegionByte = 0x7FD9;
17 return rom.size() > kRegionByte && rom.data()[kRegionByte] == 0;
18}
19namespace {
20
21// Measuring an existing stream must not fail just because an older tool
22// wrote an oversized sheet; the doctor reports that separately.
23constexpr size_t kMeasureDecodeLimit = 0x8000;
24
29
30bool Overlaps(uint32_t a_begin, uint32_t a_end, uint32_t b_begin,
31 uint32_t b_end) {
32 return a_begin < b_end && b_begin < a_end;
33}
34
35// Records the original bytes of every range it writes and restores them on
36// destruction unless Commit() is called, so a failed write-back leaves the
37// ROM buffer exactly as it was.
39 public:
40 explicit RomWriteJournal(Rom& rom) : rom_(rom) {}
43
45 if (committed_) {
46 return;
47 }
48 auto& bytes = rom_.mutable_vector();
49 for (auto it = entries_.rbegin(); it != entries_.rend(); ++it) {
50 std::copy(it->second.begin(), it->second.end(),
51 bytes.begin() + it->first);
52 }
53 }
54
55 absl::Status Write(uint32_t pc, const std::vector<uint8_t>& data) {
56 if (static_cast<size_t>(pc) + data.size() > rom_.size()) {
57 return absl::OutOfRangeError(absl::StrFormat(
58 "Write of %zu bytes at 0x%06X is past the ROM end", data.size(), pc));
59 }
60 std::vector<uint8_t> original(rom_.vector().begin() + pc,
61 rom_.vector().begin() + pc + data.size());
62 RETURN_IF_ERROR(rom_.WriteVector(static_cast<int>(pc), data));
63 entries_.emplace_back(pc, std::move(original));
64 return absl::OkStatus();
65 }
66
67 void Commit() { committed_ = true; }
68
69 private:
71 std::vector<std::pair<uint32_t, std::vector<uint8_t>>> entries_;
72 bool committed_ = false;
73};
74
75absl::Status CheckPolicyWrite(const GfxSheetWritePolicy& policy, uint32_t begin,
76 uint32_t end) {
77 if (!policy.check_write) {
78 return absl::OkStatus();
79 }
80 return policy.check_write(begin, end);
81}
82
84 std::vector<GfxSheetExtent> extents;
85 std::vector<uint16_t> unreadable;
86};
87
88OtherSheetExtents CollectOtherExtents(const Rom& rom, uint16_t skip_sheet,
89 const GfxSheetPointerTables& tables) {
90 OtherSheetExtents result;
91 for (uint16_t sheet = 0; sheet < kGfxSheetCount; ++sheet) {
92 if (sheet == skip_sheet) {
93 continue;
94 }
95 auto extent = ReadGfxSheetExtent(rom, sheet, tables);
96 if (extent.ok()) {
97 result.extents.push_back(*extent);
98 } else {
99 result.unreadable.push_back(sheet);
100 }
101 }
102 return result;
103}
104
105absl::StatusOr<uint32_t> FindFreeSpace(const Rom& rom,
106 const GfxSheetWritePolicy& policy,
107 const OtherSheetExtents& others,
108 const GfxSheetPointerTables& tables,
109 size_t size) {
110 std::vector<GfxSheetPcRange> occupied;
111 occupied.reserve(others.extents.size() + 3);
112 for (const auto& extent : others.extents) {
113 occupied.push_back(
114 {extent.pc, static_cast<uint32_t>(extent.pc + extent.stored_size)});
115 }
116 for (uint32_t table : {tables.bank, tables.high, tables.low}) {
117 occupied.push_back({table, static_cast<uint32_t>(table + kGfxSheetCount)});
118 }
119 std::sort(
120 occupied.begin(), occupied.end(),
121 [](const auto& lhs, const auto& rhs) { return lhs.begin < rhs.begin; });
122
123 std::vector<GfxSheetPcRange> regions = policy.allocation_regions;
124 std::sort(
125 regions.begin(), regions.end(),
126 [](const auto& lhs, const auto& rhs) { return lhs.begin < rhs.begin; });
127 size_t largest_gap = 0;
128 for (const auto& region : regions) {
129 if (region.begin >= region.end || region.end > rom.size()) {
130 return absl::InvalidArgumentError(absl::StrFormat(
131 "Graphics allocation region [0x%06X, 0x%06X) is empty or past the "
132 "ROM end (0x%06zX)",
133 region.begin, region.end, rom.size()));
134 }
135 uint32_t cursor = region.begin;
136 auto try_gap = [&](uint32_t gap_end) -> bool {
137 if (gap_end > cursor) {
138 largest_gap = std::max<size_t>(largest_gap, gap_end - cursor);
139 return gap_end - cursor >= size;
140 }
141 return false;
142 };
143 for (const auto& used : occupied) {
144 if (used.end <= cursor || used.begin >= region.end) {
145 continue;
146 }
147 if (try_gap(std::min(used.begin, region.end))) {
148 return cursor;
149 }
150 cursor = std::max(cursor, used.end);
151 if (cursor >= region.end) {
152 break;
153 }
154 }
155 if (cursor < region.end && try_gap(region.end)) {
156 return cursor;
157 }
158 }
159 return absl::ResourceExhaustedError(absl::StrFormat(
160 "No registered graphics free space fits %zu bytes (largest free gap: "
161 "%zu bytes)",
162 size, largest_gap));
163}
164
165absl::Status VerifyCompressedSheet(const Rom& rom, uint16_t sheet_id,
166 uint32_t expected_pc,
167 const std::vector<uint8_t>& expected,
168 size_t expected_stored_size,
169 const GfxSheetPointerTables& tables) {
170 ASSIGN_OR_RETURN(const uint32_t pc, ReadGfxSheetPc(rom, sheet_id, tables));
171 if (pc != expected_pc) {
172 return absl::DataLossError(absl::StrFormat(
173 "Sheet 0x%02X pointer reads back as 0x%06X, expected 0x%06X", sheet_id,
174 pc, expected_pc));
175 }
176 auto decoded =
177 gfx::lc_lz2::DecompressExact(rom.data(), rom.size(), pc, expected.size(),
178 /*big_endian_copy=*/false);
179 if (!decoded.ok()) {
180 return absl::DataLossError(
181 absl::StrFormat("Sheet 0x%02X does not decode after write: %s",
182 sheet_id, decoded.status().message()));
183 }
184 if (decoded->data != expected ||
185 decoded->compressed_size != expected_stored_size) {
186 return absl::DataLossError(absl::StrFormat(
187 "Sheet 0x%02X read-back differs from the edited sheet", sheet_id));
188 }
189 return absl::OkStatus();
190}
191
192} // namespace
193
194std::array<int, 4> GfxSheetBlockTiles(int block) {
195 const int top_left = (block / 8) * 32 + (block % 8) * 2;
196 return {top_left, top_left + 1, top_left + 16, top_left + 17};
197}
198
199bool IsGfxSheetBlockEmpty(const std::vector<uint8_t>& data, int block,
200 int bpp) {
201 const size_t tile_bytes = static_cast<size_t>(8 * bpp);
202 for (int tile : GfxSheetBlockTiles(block)) {
203 const size_t begin = static_cast<size_t>(tile) * tile_bytes;
204 if (begin + tile_bytes > data.size()) {
205 return false;
206 }
207 for (size_t i = begin; i < begin + tile_bytes; ++i) {
208 if (data[i] != 0) {
209 return false;
210 }
211 }
212 }
213 return true;
214}
215
217 if (sheet_id >= 115 && sheet_id <= 126) {
219 }
220 if (sheet_id == 113 || sheet_id == 114 || sheet_id >= 218) {
222 }
224}
225
227 switch (kind) {
229 return "compressed 3bpp";
231 return "raw 3bpp";
233 return "compressed 2bpp";
234 }
235 return "unknown";
236}
237
238absl::StatusOr<uint32_t> ReadGfxSheetPc(const Rom& rom, uint16_t sheet_id,
239 const GfxSheetPointerTables& tables) {
240 if (sheet_id >= kGfxSheetCount) {
241 return absl::InvalidArgumentError(
242 absl::StrFormat("Graphics sheet %d is out of range (0-%zu)", sheet_id,
243 kGfxSheetCount - 1));
244 }
245 for (uint32_t table : {tables.bank, tables.high, tables.low}) {
246 if (static_cast<size_t>(table) + kGfxSheetCount > rom.size()) {
247 return absl::OutOfRangeError(absl::StrFormat(
248 "Graphics pointer table at 0x%06X is past the ROM end", table));
249 }
250 }
251 const uint32_t snes = (rom.data()[tables.bank + sheet_id] << 16) |
252 (rom.data()[tables.high + sheet_id] << 8) |
253 rom.data()[tables.low + sheet_id];
254 if ((snes & 0xFFFF) < 0x8000) {
255 return absl::DataLossError(
256 absl::StrFormat("Sheet 0x%02X pointer $%06X is not a LoROM ROM address",
257 sheet_id, snes));
258 }
259 const uint32_t pc = SnesToPc(snes);
260 if (pc >= rom.size()) {
261 return absl::OutOfRangeError(absl::StrFormat(
262 "Sheet 0x%02X pointer $%06X (PC 0x%06X) is past the ROM end", sheet_id,
263 snes, pc));
264 }
265 return pc;
266}
267
268absl::StatusOr<GfxSheetExtent> ReadGfxSheetExtent(
269 const Rom& rom, uint16_t sheet_id, const GfxSheetPointerTables& tables) {
270 GfxSheetExtent extent;
271 extent.sheet_id = sheet_id;
272 extent.kind = GetGfxSheetStorageKind(sheet_id);
273 ASSIGN_OR_RETURN(extent.pc, ReadGfxSheetPc(rom, sheet_id, tables));
274
275 if (extent.kind == GfxSheetStorageKind::kRaw3bpp) {
276 if (static_cast<size_t>(extent.pc) + kGfxSheet3bppBytes > rom.size()) {
277 return absl::OutOfRangeError(
278 absl::StrFormat("Raw sheet 0x%02X at 0x%06X runs past the ROM end",
279 sheet_id, extent.pc));
280 }
283 return extent;
284 }
285
286 ASSIGN_OR_RETURN(auto decoded,
288 extent.pc, kMeasureDecodeLimit,
289 /*big_endian_copy=*/false));
290 extent.stored_size = decoded.compressed_size;
291 extent.decoded_size = decoded.data.size();
292 return extent;
293}
294
295absl::StatusOr<std::vector<uint8_t>> ReadGfxSheetData(
296 const Rom& rom, uint16_t sheet_id, const GfxSheetPointerTables& tables) {
297 ASSIGN_OR_RETURN(const GfxSheetExtent extent,
298 ReadGfxSheetExtent(rom, sheet_id, tables));
299 if (extent.kind == GfxSheetStorageKind::kRaw3bpp) {
300 return std::vector<uint8_t>(
301 rom.vector().begin() + extent.pc,
302 rom.vector().begin() + extent.pc + kGfxSheet3bppBytes);
303 }
304 ASSIGN_OR_RETURN(auto decoded,
306 extent.pc, kMeasureDecodeLimit,
307 /*big_endian_copy=*/false));
308 return std::move(decoded.data);
309}
310
311absl::StatusOr<GfxSheetWriteResult> WriteGfxSheet(
312 Rom& rom, uint16_t sheet_id, const std::vector<uint8_t>& snes_3bpp,
313 const GfxSheetWritePolicy& policy, const GfxSheetPointerTables& tables) {
314 if (!rom.is_loaded()) {
315 return absl::FailedPreconditionError("ROM not loaded");
316 }
317 if (sheet_id >= kGfxSheetCount) {
318 return absl::InvalidArgumentError(
319 absl::StrFormat("Graphics sheet %d is out of range (0-%zu)", sheet_id,
320 kGfxSheetCount - 1));
321 }
322 if (policy.reserved_sheets.count(sheet_id) != 0) {
323 return absl::FailedPreconditionError(absl::StrFormat(
324 "Graphics sheet 0x%02X is reserved by the project and must not be "
325 "written",
326 sheet_id));
327 }
328 const GfxSheetStorageKind kind = GetGfxSheetStorageKind(sheet_id);
330 return absl::UnimplementedError(absl::StrFormat(
331 "Graphics sheet 0x%02X is 2bpp; 2bpp sheets are read-only", sheet_id));
332 }
333 if (snes_3bpp.size() != kGfxSheet3bppBytes) {
334 return absl::InvalidArgumentError(absl::StrFormat(
335 "Sheet 0x%02X data is %zu bytes; a 3bpp sheet is exactly 0x%zX",
336 sheet_id, snes_3bpp.size(), kGfxSheet3bppBytes));
337 }
338
339 if (auto reserved = policy.reserved_blocks.find(sheet_id);
340 reserved != policy.reserved_blocks.end() && !reserved->second.empty()) {
341 auto current = ReadGfxSheetData(rom, sheet_id, tables);
342 if (!current.ok() || current->size() < kGfxSheet3bppBytes) {
343 return absl::FailedPreconditionError(absl::StrFormat(
344 "Sheet 0x%02X has reserved 16x16 blocks but its current data "
345 "cannot be read to protect them",
346 sheet_id));
347 }
348 constexpr size_t kTileBytes = 24;
349 for (uint16_t block : reserved->second) {
350 for (int tile : GfxSheetBlockTiles(block)) {
351 const size_t begin = static_cast<size_t>(tile) * kTileBytes;
352 if (begin + kTileBytes > kGfxSheet3bppBytes ||
353 !std::equal(snes_3bpp.begin() + begin,
354 snes_3bpp.begin() + begin + kTileBytes,
355 current->begin() + begin)) {
356 return absl::FailedPreconditionError(absl::StrFormat(
357 "Sheet 0x%02X 16x16 block %d is reserved by the project and "
358 "must keep its pixels",
359 sheet_id, block));
360 }
361 }
362 }
363 }
364
365 ASSIGN_OR_RETURN(const uint32_t old_pc,
366 ReadGfxSheetPc(rom, sheet_id, tables));
367 GfxSheetWriteResult result;
368 result.sheet_id = sheet_id;
369 result.old_pc = old_pc;
370
371 // Unchanged pixels leave the ROM alone. Re-encoding would produce a
372 // different (valid) stream and churn every byte of the sheet.
373 if (auto extent = ReadGfxSheetExtent(rom, sheet_id, tables); extent.ok()) {
374 auto current = ReadGfxSheetData(rom, sheet_id, tables);
375 if (current.ok() && current->size() >= kGfxSheet3bppBytes &&
376 extent->decoded_size == kGfxSheet3bppBytes &&
377 std::equal(snes_3bpp.begin(), snes_3bpp.end(), current->begin())) {
378 result.new_pc = old_pc;
379 result.old_stored_size = extent->stored_size;
380 result.new_stored_size = extent->stored_size;
381 return result;
382 }
383 }
384
385 RomWriteJournal journal(rom);
386
387 if (kind == GfxSheetStorageKind::kRaw3bpp) {
388 const uint32_t end = old_pc + static_cast<uint32_t>(kGfxSheet3bppBytes);
389 RETURN_IF_ERROR(CheckPolicyWrite(policy, old_pc, end));
390 RETURN_IF_ERROR(journal.Write(old_pc, snes_3bpp));
391 ASSIGN_OR_RETURN(auto readback, ReadGfxSheetData(rom, sheet_id, tables));
392 if (readback != snes_3bpp) {
393 return absl::DataLossError(absl::StrFormat(
394 "Raw sheet 0x%02X read-back differs from the edited sheet",
395 sheet_id));
396 }
397 journal.Commit();
398 result.new_pc = old_pc;
401 return result;
402 }
403
404 // Graphics streams use little-endian LZ source addresses (flag 0); the
405 // big-endian form is for overworld map data.
406 int encoded_size = 0;
407 std::vector<uint8_t> encoded = gfx::HyruleMagicCompress(
408 snes_3bpp.data(), static_cast<int>(snes_3bpp.size()), &encoded_size,
409 /*flag=*/0);
410 encoded.resize(encoded_size);
411 {
412 auto check = gfx::lc_lz2::DecompressExact(encoded.data(), encoded.size(), 0,
414 /*big_endian_copy=*/false);
415 if (!check.ok() || check->data != snes_3bpp ||
416 check->compressed_size != encoded.size()) {
417 return absl::InternalError(absl::StrFormat(
418 "Encoding sheet 0x%02X did not round-trip; nothing was written",
419 sheet_id));
420 }
421 }
422 result.new_stored_size = encoded.size();
423
424 const OtherSheetExtents others = CollectOtherExtents(rom, sheet_id, tables);
425 auto old_extent = ReadGfxSheetExtent(rom, sheet_id, tables);
426 bool fits_in_place = false;
427 if (old_extent.ok()) {
428 result.old_stored_size = old_extent->stored_size;
429 fits_in_place = encoded.size() <= old_extent->stored_size;
430 const uint32_t old_end =
431 old_pc + static_cast<uint32_t>(old_extent->stored_size);
432 for (const auto& other : others.extents) {
433 // Another sheet sharing these bytes would change with this write.
434 if (Overlaps(old_pc, old_end, other.pc,
435 other.pc + static_cast<uint32_t>(other.stored_size))) {
436 fits_in_place = false;
437 break;
438 }
439 }
440 }
441
442 uint32_t target = old_pc;
443 if (!fits_in_place) {
444 if (policy.allocation_regions.empty()) {
445 return absl::FailedPreconditionError(absl::StrFormat(
446 "Sheet 0x%02X needs %zu bytes but its current stream holds %zu, "
447 "and no graphics free space is registered "
448 "(hack manifest graphics_sheet_regions.allocation_regions)",
449 sheet_id, encoded.size(), result.old_stored_size));
450 }
451 if (!others.unreadable.empty()) {
452 std::vector<std::string> ids;
453 for (uint16_t id : others.unreadable) {
454 ids.push_back(absl::StrFormat("0x%02X", id));
455 }
456 return absl::FailedPreconditionError(absl::StrFormat(
457 "Cannot compute graphics free space: sheet(s) %s do not decode",
458 absl::StrJoin(ids, ", ")));
459 }
461 target, FindFreeSpace(rom, policy, others, tables, encoded.size()));
462 }
463
464 RETURN_IF_ERROR(CheckPolicyWrite(
465 policy, target, target + static_cast<uint32_t>(encoded.size())));
466 RETURN_IF_ERROR(journal.Write(target, encoded));
467
468 if (target != old_pc) {
469 const uint8_t old_bank = rom.data()[tables.bank + sheet_id];
470 uint8_t bank = static_cast<uint8_t>(target >> 15);
471 // Keep the sheet's FastROM form, and never point into the WRAM banks.
472 if ((old_bank & 0x80) != 0 || bank >= 0x7E) {
473 bank |= 0x80;
474 }
475 const uint16_t addr = static_cast<uint16_t>(0x8000 | (target & 0x7FFF));
476 const std::pair<uint32_t, uint8_t> pointer_bytes[] = {
477 {tables.bank + sheet_id, bank},
478 {tables.high + sheet_id, static_cast<uint8_t>(addr >> 8)},
479 {tables.low + sheet_id, static_cast<uint8_t>(addr & 0xFF)}};
480 for (const auto& [pc, value] : pointer_bytes) {
481 RETURN_IF_ERROR(CheckPolicyWrite(policy, pc, pc + 1));
482 RETURN_IF_ERROR(journal.Write(pc, {value}));
483 }
485 }
486 result.new_pc = target;
487
488 RETURN_IF_ERROR(VerifyCompressedSheet(rom, sheet_id, target, snes_3bpp,
489 encoded.size(), tables));
490 journal.Commit();
491 LOG_INFO("GfxSheetStorage", "Wrote sheet %02X: %zu bytes %s at 0x%06X",
492 sheet_id, encoded.size(),
493 result.placement == GfxSheetPlacement::kRelocated ? "relocated"
494 : "in place",
495 target);
496 return result;
497}
498
499} // namespace yaze::zelda3
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:49
const auto & vector() const
Definition rom.h:205
auto data() const
Definition rom.h:201
auto size() const
Definition rom.h:200
bool is_loaded() const
Definition rom.h:187
RomWriteJournal & operator=(const RomWriteJournal &)=delete
std::vector< std::pair< uint32_t, std::vector< uint8_t > > > entries_
absl::Status Write(uint32_t pc, const std::vector< uint8_t > &data)
Locates graphics sheets in the ROM and writes edited sheets back.
#define LOG_INFO(category, format,...)
Definition log.h:136
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
absl::StatusOr< ExactDecodeResult > DecompressExact(const uint8_t *data, size_t data_size, size_t offset, size_t max_output, bool big_endian_copy)
Decodes one LC-LZ2 stream with the semantics of the ALTTP bank-00 decompressor (Decompress at $00:E79...
std::vector< uint8_t > HyruleMagicCompress(uint8_t const *const src, int const oldsize, int *const size, int const flag)
absl::StatusOr< uint32_t > FindFreeSpace(const Rom &rom, const GfxSheetWritePolicy &policy, const OtherSheetExtents &others, const GfxSheetPointerTables &tables, size_t size)
bool Overlaps(uint32_t a_begin, uint32_t a_end, uint32_t b_begin, uint32_t b_end)
absl::Status CheckPolicyWrite(const GfxSheetWritePolicy &policy, uint32_t begin, uint32_t end)
absl::Status VerifyCompressedSheet(const Rom &rom, uint16_t sheet_id, uint32_t expected_pc, const std::vector< uint8_t > &expected, size_t expected_stored_size, const GfxSheetPointerTables &tables)
OtherSheetExtents CollectOtherExtents(const Rom &rom, uint16_t skip_sheet, const GfxSheetPointerTables &tables)
Zelda 3 specific classes and functions.
absl::StatusOr< GfxSheetExtent > ReadGfxSheetExtent(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
constexpr size_t kGfxSheet3bppBytes
std::array< int, 4 > GfxSheetBlockTiles(int block)
absl::StatusOr< std::vector< uint8_t > > ReadGfxSheetData(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
Returns the sheet's decoded bytes (SNES planar data).
absl::StatusOr< GfxSheetWriteResult > WriteGfxSheet(Rom &rom, uint16_t sheet_id, const std::vector< uint8_t > &snes_3bpp, const GfxSheetWritePolicy &policy, const GfxSheetPointerTables &tables)
Writes one edited 3bpp sheet to the ROM buffer.
constexpr size_t kGfxSheet2bppBytes
bool IsGfxSheetBlockEmpty(const std::vector< uint8_t > &data, int block, int bpp)
GfxSheetStorageKind GetGfxSheetStorageKind(uint16_t sheet_id)
constexpr size_t kGfxSheetCount
bool UsesJapaneseGfxSheetTables(const Rom &rom)
const char * GfxSheetStorageKindName(GfxSheetStorageKind kind)
absl::StatusOr< uint32_t > ReadGfxSheetPc(const Rom &rom, uint16_t sheet_id, const GfxSheetPointerTables &tables)
Resolves a sheet's pointer. Does not read the sheet data.
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
Where one sheet's bytes live and how many bytes they occupy.
PC offsets of the bank, high and low byte pointer tables.
Rules a sheet write must follow.
std::vector< GfxSheetPcRange > allocation_regions
std::function< absl::Status(uint32_t begin, uint32_t end)> check_write
std::map< uint16_t, std::vector< uint16_t > > reserved_blocks