fix(gfx): convert indexed sheets to SNES planar
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
#include "graphics_editor.h"
|
||||
|
||||
// C++ standard library headers
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
// Third-party library headers
|
||||
@@ -229,22 +230,6 @@ absl::Status GraphicsEditor::Save() {
|
||||
compressed = false;
|
||||
}
|
||||
|
||||
// Convert 8BPP bitmap data to SNES indexed format
|
||||
auto indexed_data = gfx::Bpp8SnesToIndexed(sheet.vector(), bpp);
|
||||
|
||||
std::vector<uint8_t> final_data;
|
||||
if (compressed) {
|
||||
// Compress using Hyrule Magic LC-LZ2
|
||||
int compressed_size = 0;
|
||||
auto compressed_data = gfx::HyruleMagicCompress(
|
||||
indexed_data.data(), static_cast<int>(indexed_data.size()),
|
||||
&compressed_size, 1);
|
||||
final_data.assign(compressed_data.begin(),
|
||||
compressed_data.begin() + compressed_size);
|
||||
} else {
|
||||
final_data = std::move(indexed_data);
|
||||
}
|
||||
|
||||
// Calculate ROM offset for this sheet
|
||||
// Get version constants from game_data
|
||||
auto version_constants = zelda3::kVersionConstantsMap.at(game_data()->version);
|
||||
@@ -254,6 +239,47 @@ absl::Status GraphicsEditor::Save() {
|
||||
version_constants.kOverworldGfxPtr2,
|
||||
version_constants.kOverworldGfxPtr3, rom_->size());
|
||||
|
||||
// Convert 8BPP bitmap data to SNES planar format
|
||||
auto snes_tile_data = gfx::IndexedToSnesSheet(sheet.vector(), bpp);
|
||||
|
||||
constexpr size_t kDecompressedSheetSize = 0x800;
|
||||
std::vector<uint8_t> base_data;
|
||||
if (compressed) {
|
||||
auto decomp_result = gfx::lc_lz2::DecompressV2(
|
||||
rom_->data(), offset, static_cast<int>(kDecompressedSheetSize), 1,
|
||||
rom_->size());
|
||||
if (!decomp_result.ok()) {
|
||||
return decomp_result.status();
|
||||
}
|
||||
base_data = std::move(*decomp_result);
|
||||
} else {
|
||||
auto read_result =
|
||||
rom_->ReadByteVector(offset, kDecompressedSheetSize);
|
||||
if (!read_result.ok()) {
|
||||
return read_result.status();
|
||||
}
|
||||
base_data = std::move(*read_result);
|
||||
}
|
||||
|
||||
if (base_data.size() < snes_tile_data.size()) {
|
||||
base_data.resize(snes_tile_data.size(), 0);
|
||||
}
|
||||
std::copy(snes_tile_data.begin(), snes_tile_data.end(),
|
||||
base_data.begin());
|
||||
|
||||
std::vector<uint8_t> final_data;
|
||||
if (compressed) {
|
||||
// Compress using Hyrule Magic LC-LZ2
|
||||
int compressed_size = 0;
|
||||
auto compressed_data = gfx::HyruleMagicCompress(
|
||||
base_data.data(), static_cast<int>(base_data.size()),
|
||||
&compressed_size, 1);
|
||||
final_data.assign(compressed_data.begin(),
|
||||
compressed_data.begin() + compressed_size);
|
||||
} else {
|
||||
final_data = std::move(base_data);
|
||||
}
|
||||
|
||||
// Write data to ROM buffer
|
||||
for (size_t i = 0; i < final_data.size(); i++) {
|
||||
rom_->WriteByte(offset + i, final_data[i]);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#include "snes_tile.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace yaze {
|
||||
namespace gfx {
|
||||
@@ -128,8 +129,8 @@ std::vector<uint8_t> ConvertBpp(std::span<uint8_t> tiles, uint32_t from_bpp,
|
||||
return converted;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> SnesTo8bppSheet(std::span<uint8_t> sheet, int bpp,
|
||||
int num_sheets) {
|
||||
std::vector<uint8_t> SnesTo8bppSheet(std::span<uint8_t> sheet, int bpp,
|
||||
int num_sheets) {
|
||||
int xx = 0; // positions where we are at on the sheet
|
||||
int yy = 0;
|
||||
int pos = 0;
|
||||
@@ -196,11 +197,67 @@ std::vector<uint8_t> SnesTo8bppSheet(std::span<uint8_t> sheet, int bpp,
|
||||
ypos = 0;
|
||||
}
|
||||
}
|
||||
return sheet_buffer_out;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Bpp8SnesToIndexed(std::vector<uint8_t> data,
|
||||
uint64_t bpp) {
|
||||
return sheet_buffer_out;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> IndexedToSnesSheet(std::span<const uint8_t> sheet, int bpp,
|
||||
int num_sheets) {
|
||||
if (sheet.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int tiles_per_row = kTilesheetWidth / 8;
|
||||
const int default_tile_rows = (bpp == 2) ? 8 : 4;
|
||||
const int computed_tile_rows =
|
||||
static_cast<int>(sheet.size()) / (kTilesheetWidth * 8);
|
||||
const int tile_rows = (computed_tile_rows > 0)
|
||||
? std::max(default_tile_rows, computed_tile_rows)
|
||||
: default_tile_rows;
|
||||
const int tiles_per_sheet = tiles_per_row * tile_rows;
|
||||
const int total_tiles = tiles_per_sheet * num_sheets;
|
||||
const int bytes_per_tile = bpp * 8;
|
||||
const uint8_t max_color =
|
||||
static_cast<uint8_t>((1u << static_cast<uint8_t>(bpp)) - 1u);
|
||||
|
||||
std::vector<uint8_t> output(total_tiles * bytes_per_tile, 0);
|
||||
|
||||
int xx = 0;
|
||||
int yy = 0;
|
||||
int pos = 0;
|
||||
int ypos = 0;
|
||||
|
||||
for (int i = 0; i < total_tiles; i++) {
|
||||
snes_tile8 tile = {};
|
||||
|
||||
for (int y = 0; y < 8; y++) {
|
||||
for (int x = 0; x < 8; x++) {
|
||||
const int index =
|
||||
(x + xx) + (y * kTilesheetWidth) + (yy * kTilesheetWidth * 8);
|
||||
if (index >= 0 && index < static_cast<int>(sheet.size())) {
|
||||
tile.data[y * 8 + x] = sheet[index] & max_color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto packed_tile = PackBppTile(tile, bpp);
|
||||
std::copy(packed_tile.begin(), packed_tile.end(),
|
||||
output.begin() + (pos * bytes_per_tile));
|
||||
|
||||
pos++;
|
||||
ypos++;
|
||||
xx += 8;
|
||||
if (ypos >= tiles_per_row) {
|
||||
yy++;
|
||||
xx = 0;
|
||||
ypos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Bpp8SnesToIndexed(std::vector<uint8_t> data,
|
||||
uint64_t bpp) {
|
||||
// 3BPP
|
||||
// [r0,bp1],[r0,bp2],[r1,bp1],[r1,bp2],[r2,bp1],[r2,bp2],[r3,bp1],[r3,bp2]
|
||||
// [r4,bp1],[r4,bp2],[r5,bp1],[r5,bp2],[r6,bp1],[r6,bp2],[r7,bp1],[r7,bp2]
|
||||
|
||||
@@ -20,10 +20,12 @@ constexpr int kTilesheetDepth = 8;
|
||||
constexpr uint8_t kGraphicsBitmap[8] = {0x80, 0x40, 0x20, 0x10,
|
||||
0x08, 0x04, 0x02, 0x01};
|
||||
|
||||
std::vector<uint8_t> SnesTo8bppSheet(std::span<uint8_t> sheet, int bpp,
|
||||
int num_sheets = 1);
|
||||
std::vector<uint8_t> Bpp8SnesToIndexed(std::vector<uint8_t> data,
|
||||
uint64_t bpp = 0);
|
||||
std::vector<uint8_t> SnesTo8bppSheet(std::span<uint8_t> sheet, int bpp,
|
||||
int num_sheets = 1);
|
||||
std::vector<uint8_t> IndexedToSnesSheet(std::span<const uint8_t> sheet,
|
||||
int bpp, int num_sheets = 1);
|
||||
std::vector<uint8_t> Bpp8SnesToIndexed(std::vector<uint8_t> data,
|
||||
uint64_t bpp = 0);
|
||||
|
||||
snes_tile8 UnpackBppTile(std::span<uint8_t> data, const uint32_t offset,
|
||||
const uint32_t bpp);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
@@ -10,9 +11,11 @@
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gfx/util/compression.h"
|
||||
#include "rom/rom.h"
|
||||
#include "test/test_utils.h"
|
||||
#include "zelda3/game_data.h"
|
||||
#include "zelda3/game_data.h"
|
||||
#include "testing.h"
|
||||
#include "zelda.h"
|
||||
|
||||
namespace yaze {
|
||||
namespace test {
|
||||
@@ -30,18 +33,10 @@ namespace test {
|
||||
class GraphicsEditorSaveTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Skip tests if ROM is not available
|
||||
if (getenv("YAZE_SKIP_ROM_TESTS")) {
|
||||
GTEST_SKIP() << "ROM tests disabled";
|
||||
}
|
||||
|
||||
// Get ROM path from environment or use default (vanilla.sfc to avoid edited ROMs)
|
||||
const char* rom_path_env = getenv("YAZE_TEST_ROM_PATH");
|
||||
vanilla_rom_path_ = rom_path_env ? rom_path_env : "vanilla.sfc";
|
||||
|
||||
if (!std::filesystem::exists(vanilla_rom_path_)) {
|
||||
GTEST_SKIP() << "Test ROM not found: " << vanilla_rom_path_;
|
||||
}
|
||||
yaze::test::TestRomManager::SkipIfRomMissing(
|
||||
yaze::test::RomRole::kVanilla, "GraphicsEditorSaveTest");
|
||||
vanilla_rom_path_ =
|
||||
yaze::test::TestRomManager::GetRomPath(yaze::test::RomRole::kVanilla);
|
||||
|
||||
// Create test ROM copies
|
||||
test_rom_path_ = "test_graphics_edit.sfc";
|
||||
@@ -66,6 +61,19 @@ class GraphicsEditorSaveTest : public ::testing::Test {
|
||||
}
|
||||
}
|
||||
|
||||
static int GetSheetBpp(uint16_t sheet_id) {
|
||||
if (sheet_id == 113 || sheet_id == 114 || sheet_id >= 218) {
|
||||
return 2;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
|
||||
static uint8_t NextPixelValue(uint16_t sheet_id, uint8_t current,
|
||||
uint8_t delta = 1) {
|
||||
const uint8_t max_colors = static_cast<uint8_t>(1u << GetSheetBpp(sheet_id));
|
||||
return static_cast<uint8_t>((current + delta) % max_colors);
|
||||
}
|
||||
|
||||
// Helper to load ROM and verify basic integrity
|
||||
static absl::Status LoadAndVerifyROM(const std::string& path,
|
||||
std::unique_ptr<Rom>& rom) {
|
||||
@@ -107,43 +115,63 @@ class GraphicsEditorSaveTest : public ::testing::Test {
|
||||
}
|
||||
|
||||
// Determine BPP and compression based on sheet range
|
||||
int bpp = 3; // Default 3BPP
|
||||
const int bpp = GetSheetBpp(sheet_id);
|
||||
bool compressed = true;
|
||||
|
||||
// Sheets 113-114, 218+ are 2BPP
|
||||
if (sheet_id == 113 || sheet_id == 114 || sheet_id >= 218) {
|
||||
bpp = 2;
|
||||
}
|
||||
|
||||
// Sheets 115-126 are uncompressed
|
||||
if (sheet_id >= 115 && sheet_id <= 126) {
|
||||
compressed = false;
|
||||
}
|
||||
|
||||
// Convert 8BPP bitmap data to SNES indexed format
|
||||
auto indexed_data = gfx::Bpp8SnesToIndexed(sheet.vector(), bpp);
|
||||
// Calculate ROM offset for this sheet
|
||||
auto version = zelda3_detect_version(rom.data(), rom.size());
|
||||
auto vc_it = zelda3::kVersionConstantsMap.find(version);
|
||||
if (vc_it == zelda3::kVersionConstantsMap.end() ||
|
||||
vc_it->second.kOverworldGfxPtr1 == 0) {
|
||||
vc_it = zelda3::kVersionConstantsMap.find(zelda3_version::US);
|
||||
}
|
||||
const auto& vc = vc_it->second;
|
||||
uint32_t offset = zelda3::GetGraphicsAddress(
|
||||
rom.data(), static_cast<uint8_t>(sheet_id),
|
||||
vc.kOverworldGfxPtr1, vc.kOverworldGfxPtr2,
|
||||
vc.kOverworldGfxPtr3, rom.size());
|
||||
|
||||
// Convert 8BPP bitmap data to SNES planar format
|
||||
auto snes_tile_data = gfx::IndexedToSnesSheet(sheet.vector(), bpp);
|
||||
|
||||
constexpr size_t kDecompressedSheetSize = 0x800;
|
||||
std::vector<uint8_t> base_data;
|
||||
if (compressed) {
|
||||
auto decomp_result = gfx::lc_lz2::DecompressV2(
|
||||
rom.data(), offset, static_cast<int>(kDecompressedSheetSize), 1,
|
||||
rom.size());
|
||||
RETURN_IF_ERROR(decomp_result.status());
|
||||
base_data = std::move(*decomp_result);
|
||||
} else {
|
||||
auto read_result = rom.ReadByteVector(offset, kDecompressedSheetSize);
|
||||
RETURN_IF_ERROR(read_result.status());
|
||||
base_data = std::move(*read_result);
|
||||
}
|
||||
|
||||
if (base_data.size() < snes_tile_data.size()) {
|
||||
base_data.resize(snes_tile_data.size(), 0);
|
||||
}
|
||||
std::copy(snes_tile_data.begin(), snes_tile_data.end(),
|
||||
base_data.begin());
|
||||
|
||||
std::vector<uint8_t> final_data;
|
||||
if (compressed) {
|
||||
// Compress using Hyrule Magic LC-LZ2
|
||||
int compressed_size = 0;
|
||||
auto compressed_data = gfx::HyruleMagicCompress(
|
||||
indexed_data.data(), static_cast<int>(indexed_data.size()),
|
||||
base_data.data(), static_cast<int>(base_data.size()),
|
||||
&compressed_size, 1);
|
||||
final_data.assign(compressed_data.begin(),
|
||||
compressed_data.begin() + compressed_size);
|
||||
} else {
|
||||
final_data = std::move(indexed_data);
|
||||
final_data = std::move(base_data);
|
||||
}
|
||||
|
||||
// Calculate ROM offset for this sheet
|
||||
// Use JP version constants as default for vanilla ROM
|
||||
const auto& vc = zelda3::kVersionConstantsMap.at(zelda3_version::JP);
|
||||
uint32_t offset = zelda3::GetGraphicsAddress(
|
||||
rom.data(), static_cast<uint8_t>(sheet_id),
|
||||
vc.kOverworldGfxPtr1, vc.kOverworldGfxPtr2,
|
||||
vc.kOverworldGfxPtr3, rom.size());
|
||||
|
||||
// Write data to ROM buffer
|
||||
for (size_t i = 0; i < final_data.size(); i++) {
|
||||
RETURN_IF_ERROR(rom.WriteByte(offset + i, final_data[i]));
|
||||
@@ -174,7 +202,7 @@ TEST_F(GraphicsEditorSaveTest, SingleSheetEdit_SaveAndReload) {
|
||||
uint8_t original_pixel = sheet.GetPixel(0, 0);
|
||||
|
||||
// Modify pixel (cycle to next value, wrapping at 16 for 4-bit indexed)
|
||||
uint8_t new_pixel = (original_pixel + 1) % 16;
|
||||
uint8_t new_pixel = NextPixelValue(0, original_pixel);
|
||||
sheet.WriteToPixel(0, 0, new_pixel);
|
||||
|
||||
// Verify modification took effect in memory
|
||||
@@ -210,12 +238,19 @@ TEST_F(GraphicsEditorSaveTest, MultipleSheetEdit_Atomicity) {
|
||||
|
||||
// Modify sheets 0, 50, and 100 with distinct values
|
||||
const std::vector<uint16_t> test_sheets = {0, 50, 100};
|
||||
const std::vector<uint8_t> test_values = {5, 10, 15};
|
||||
std::vector<uint8_t> test_values;
|
||||
test_values.reserve(test_sheets.size());
|
||||
|
||||
for (size_t i = 0; i < test_sheets.size(); i++) {
|
||||
auto& sheet = sheets[test_sheets[i]];
|
||||
if (sheet.is_active()) {
|
||||
sheet.WriteToPixel(0, 0, test_values[i]);
|
||||
uint8_t original_pixel = sheet.GetPixel(0, 0);
|
||||
uint8_t new_pixel =
|
||||
NextPixelValue(test_sheets[i], original_pixel, static_cast<uint8_t>(i + 1));
|
||||
sheet.WriteToPixel(0, 0, new_pixel);
|
||||
test_values.push_back(new_pixel);
|
||||
} else {
|
||||
test_values.push_back(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +303,7 @@ TEST_F(GraphicsEditorSaveTest, CompressionIntegrity_LZ2Sheets) {
|
||||
|
||||
// Modify a single pixel
|
||||
uint8_t original_pixel = sheet.GetPixel(4, 4);
|
||||
uint8_t new_pixel = (original_pixel + 7) % 16;
|
||||
uint8_t new_pixel = NextPixelValue(test_sheet, original_pixel, 1);
|
||||
sheet.WriteToPixel(4, 4, new_pixel);
|
||||
|
||||
// Save and reload
|
||||
@@ -312,7 +347,7 @@ TEST_F(GraphicsEditorSaveTest, UncompressedSheets_SaveCorrectly) {
|
||||
|
||||
// Modify pixel
|
||||
uint8_t original_pixel = sheet.GetPixel(0, 0);
|
||||
uint8_t new_pixel = (original_pixel + 3) % 16;
|
||||
uint8_t new_pixel = NextPixelValue(test_sheet, original_pixel, 1);
|
||||
sheet.WriteToPixel(0, 0, new_pixel);
|
||||
|
||||
// Save and reload
|
||||
@@ -349,7 +384,9 @@ TEST_F(GraphicsEditorSaveTest, SaveWithoutCorruption_AdjacentData) {
|
||||
|
||||
// Modify only sheet 50
|
||||
if (sheets[50].is_active()) {
|
||||
sheets[50].WriteToPixel(0, 0, 8);
|
||||
uint8_t original_pixel = sheets[50].GetPixel(0, 0);
|
||||
uint8_t new_pixel = NextPixelValue(50, original_pixel, 1);
|
||||
sheets[50].WriteToPixel(0, 0, new_pixel);
|
||||
ASSERT_OK(SaveSheetToRom(*rom, 50));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user