diff --git a/docs/A1-testing-guide.md b/docs/A1-testing-guide.md index ef46c87f..c472a3e4 100644 --- a/docs/A1-testing-guide.md +++ b/docs/A1-testing-guide.md @@ -1,55 +1,100 @@ # A1 - Testing Guide -This guide provides a comprehensive overview of the testing framework for the yaze project, including unit tests, integration tests, and the end-to-end GUI automation system. +This guide provides a comprehensive overview of the testing framework for the yaze project, including the test organization, execution methods, and the end-to-end GUI automation system. -## 1. Test Categories +## 1. Test Organization -Tests are organized into three categories using labels to allow for flexible and efficient execution. +The test suite is organized into a clear directory structure that separates tests by their purpose and dependencies. This is the primary way to understand the nature of a test. -### Stable Tests (STABLE) -**Always run in CI/CD - Required for releases** +``` +test/ +├── unit/ # Unit tests for individual components +│ ├── core/ # Core functionality (asar, hex utils) +│ ├── cli/ # Command-line interface tests +│ ├── emu/ # Emulator component tests +│ ├── gfx/ # Graphics system (tiles, palettes) +│ ├── gui/ # GUI widget tests +│ ├── rom/ # ROM data structure tests +│ └── zelda3/ # Game-specific logic tests +├── integration/ # Tests for interactions between components +│ ├── ai/ # AI agent and vision tests +│ ├── editor/ # Editor integration tests +│ └── zelda3/ # Game-specific integration tests (ROM-dependent) +├── e2e/ # End-to-end user workflow tests (GUI-driven) +│ ├── rom_dependent/ # E2E tests requiring a ROM +│ └── zscustomoverworld/ # ZSCustomOverworld upgrade E2E tests +├── benchmarks/ # Performance benchmarks +├── mocks/ # Mock objects for isolating tests +└── assets/ # Test assets (patches, data) +``` -- **AsarWrapperTest**: Core Asar functionality tests -- **SnesTileTest**: SNES tile format handling -- **CompressionTest**: Data compression/decompression -- **SnesPaletteTest**: SNES palette operations -- **HexTest**: Hexadecimal utilities -- **AsarIntegrationTest**: Asar integration without ROM dependencies +## 2. Test Categories -**Characteristics:** -- Fast execution (< 30 seconds total) -- No external dependencies (ROMs, complex setup) -- High reliability and deterministic results +Based on the directory structure, tests fall into the following categories: -### ROM-Dependent Tests (ROM_DEPENDENT) -**Only run in development with available ROM files** +### Unit Tests (`unit/`) +- **Purpose**: To test individual classes or functions in isolation. +- **Characteristics**: + - Fast, self-contained, and reliable. + - No external dependencies (e.g., ROM files, running GUI). + - Form the core of the CI/CD validation pipeline. -- **AsarRomIntegrationTest**: Real ROM patching and symbol extraction -- **ROM-based integration tests**: Tests requiring actual game ROM files +### Integration Tests (`integration/`) +- **Purpose**: To verify that different components of the application work together correctly. +- **Characteristics**: + - May require a real ROM file (especially those in `integration/zelda3/`). These are considered "ROM-dependent". + - Test interactions between modules, such as the `asar` wrapper and the `Rom` class, or AI services with the GUI controller. + - Slower than unit tests but crucial for catching bugs at module boundaries. -**Characteristics:** -- Require specific ROM files to be present -- Test real-world functionality -- Automatically skipped in CI if ROM files unavailable +### End-to-End (E2E) Tests (`e2e/`) +- **Purpose**: To simulate a full user workflow from start to finish. +- **Characteristics**: + - Driven by the **ImGui Test Engine**. + - Almost always require a running GUI and often a real ROM. + - The slowest but most comprehensive tests, validating the user experience. + - Includes smoke tests, canvas interactions, and complex workflows like ZSCustomOverworld upgrades. -### Experimental Tests (EXPERIMENTAL) -**Run separately, allowed to fail** +### Benchmarks (`benchmarks/`) +- **Purpose**: To measure and track the performance of critical code paths, particularly in the graphics system. +- **Characteristics**: + - Not focused on correctness but on speed and efficiency. + - Run manually or in specialized CI jobs to prevent performance regressions. -- **CpuTest**: 65816 CPU emulation tests -- **Spc700Test**: SPC700 audio processor tests -- **ApuTest**: Audio Processing Unit tests -- **PpuTest**: Picture Processing Unit tests +## 3. Running Tests -**Characteristics:** -- May be unstable due to emulation complexity -- Test advanced/experimental features -- Allowed to fail without blocking releases +### Using the Enhanced Test Runner (`yaze_test`) -## 2. Running Tests +The most flexible way to run tests is by using the `yaze_test` executable directly. It provides flags to filter tests by category, which is ideal for development and AI agent workflows. -### Using CMake Presets +```bash +# First, build the test executable +cmake --build build_ai --target yaze_test -The easiest way to run tests is with `ctest` presets. +# Run all tests +./build_ai/bin/yaze_test + +# Run only unit tests +./build_ai/bin/yaze_test --unit + +# Run only integration tests +./build_ai/bin/yaze_test --integration + +# Run E2E tests (requires a GUI) +./build_ai/bin/yaze_test --e2e --show-gui + +# Run ROM-dependent tests with a specific ROM +./build_ai/bin/yaze_test --rom-dependent --rom-path /path/to/zelda3.sfc + +# Run tests matching a specific pattern (e.g., all Asar tests) +./build_ai/bin/yaze_test "*Asar*" + +# Get a full list of options +./build_ai/bin/yaze_test --help +``` + +### Using CTest and CMake Presets + +For CI/CD or a more traditional workflow, you can use `ctest` with CMake presets. ```bash # Configure a development build (enables ROM-dependent tests) @@ -58,157 +103,48 @@ cmake --preset mac-dev -DYAZE_TEST_ROM_PATH=/path/to/your/zelda3.sfc # Build the tests cmake --build --preset mac-dev --target yaze_test -# Run stable tests (fast, run in CI) +# Run stable tests (fast, primarily unit tests) ctest --preset dev -# Run all tests, including ROM-dependent and experimental +# Run all tests, including ROM-dependent and E2E ctest --preset all ``` -### Manual Execution +## 4. Writing Tests -You can also run tests by invoking the test executable directly or using CTest with labels. +When adding new tests, place them in the appropriate directory based on their purpose and dependencies. -```bash -# Run all tests via the executable -./build/bin/yaze_test +- **New class `MyClass`?** Add `test/unit/my_class_test.cc`. +- **Testing `MyClass` with a real ROM?** Add `test/integration/my_class_rom_test.cc`. +- **Testing a full UI workflow involving `MyClass`?** Add `test/e2e/my_class_workflow_test.cc`. -# Run only stable tests using CTest labels -ctest --test-dir build --label-regex "STABLE" +## 5. E2E GUI Testing Framework -# Run tests matching a name -ctest --test-dir build -R "AsarWrapperTest" - -# Exclude ROM-dependent tests -ctest --test-dir build --label-exclude "ROM_DEPENDENT" -``` - -## 3. Writing Tests - -### Stable Tests -```cpp -TEST(SnesTileTest, UnpackBppTile) { - std::vector tile_data = {0xAA, 0x55, 0xAA, 0x55}; - std::vector result = UnpackBppTile(tile_data, 2); - EXPECT_EQ(result.size(), 64); - // Test specific pixel values... -} -``` - -### ROM-Dependent Tests -```cpp -YAZE_ROM_TEST(AsarIntegration, RealRomPatching) { - auto rom_data = TestRomManager::LoadTestRom(); - if (!rom_data.has_value()) { - GTEST_SKIP() << "ROM file not available"; - } - - AsarWrapper wrapper; - wrapper.Initialize(); - - auto result = wrapper.ApplyPatch("test.asm", *rom_data); - EXPECT_TRUE(result.ok()); -} -``` - -## 4. E2E GUI Testing Framework - -An agent-friendly, end-to-end testing framework built on `ImGuiTestEngine` to automate UI interaction testing for the YAZE editor. +The E2E framework uses `ImGuiTestEngine` to automate UI interactions. ### Architecture -**Test Execution Flow**: -1. `z3ed test-gui` command invokes the modified `yaze_test` executable -2. `yaze_test` initializes an application window and `ImGuiTestEngine` -3. Tests are registered and executed against the live GUI -4. Results are reported back with detailed logs and assertions +- **`test/yaze_test.cc`**: The main test runner that can initialize a GUI for E2E tests. +- **`test/e2e/`**: Contains all E2E test files, such as: + - `framework_smoke_test.cc`: Basic infrastructure verification. + - `canvas_selection_test.cc`: Canvas interaction tests. + - `dungeon_editor_tests.cc`: UI tests for the dungeon editor. +- **`test/test_utils.h`**: Provides high-level helper functions for common actions like loading a ROM (`LoadRomInTest`) or opening an editor (`OpenEditorInTest`). -**Key Components**: -- `test/yaze_test.cc` - Main test executable with GUI initialization -- `test/e2e/framework_smoke_test.cc` - Basic infrastructure verification -- `test/e2e/canvas_selection_test.cc` - Canvas interaction tests -- `test/test_utils.h` - High-level action wrappers (LoadRomInTest, OpenEditorInTest, etc.) +### Running GUI Tests -### Widget Registration for Automation - -All modern UI components are fully integrated with the ImGui Test Engine for seamless automation. - -**Toolbar Components:** -All toolbar buttons are automatically registered with descriptive paths: -- `ModeButton:Pan (1)` -- `ModeButton:Draw (2)` -- `ToolbarAction:Toggle Tile16 Selector` -- `ToolbarAction:Open Tile16 Editor` - -**Editor Cards:** -All EditorCard windows are registered as discoverable windows: -- `EditorCard:Tile16 Selector` -- `EditorCard:Area Graphics` - -**State Introspection:** -Card visibility states are tracked for test automation: -- `OverworldToolbar/state:tile16_selector_visible` - -### Running GUI Tests with `z3ed` - -The `z3ed` CLI provides a powerful interface for running GUI tests. +To run E2E tests and see the GUI interactions, use the `--show-gui` flag. ```bash -# Run all E2E tests -z3ed gui replay --ci-mode +# Run all E2E tests with the GUI visible +./build_ai/bin/yaze_test --e2e --show-gui -# Run a specific test suite from a YAML file -z3ed gui replay test_overworld_workflow.yaml --ci-mode - -# Click a specific button -z3ed gui click "ModeButton:Draw (2)" - -# Wait for a window to become visible -z3ed gui wait "window_visible:Tile16 Selector" - -# Assert that a card is visible -z3ed gui assert "visible:EditorCard:Tile16 Selector" +# Run a specific E2E test by name +./build_ai/bin/yaze_test --show-gui --gtest_filter="*DungeonEditorSmokeTest" ``` -### Widget Discovery +### Widget Discovery and AI Integration -You can discover all registered UI elements for scripting and testing. +The GUI testing framework is designed for AI agent automation. All major UI elements are registered with stable IDs, allowing an agent to "discover" and interact with them programmatically via the `z3ed` CLI. -```bash -# List all widgets in the Overworld window -z3ed gui discover --window="Overworld" --format=yaml > overworld_widgets.yaml - -# Find all toolbar actions -z3ed gui discover --path-prefix="ToolbarAction:" - -# Find all editor cards -z3ed gui discover --path-prefix="EditorCard:" -``` - -### Integration with AI Agent - -The agent can leverage the GUI testing framework to perform actions. - -1. **Discover UI Elements**: `Agent> describe gui` -2. **Interact with UI**: `Agent> click toolbar button "Toggle Tile16 Selector"` -3. **Monitor State**: `Agent> check if "Tile16 Selector" is visible` - -### GUI Automation Scenarios - -Scenarios are defined in JSONL files, where each line is a JSON action. - -**Example Scenario (`overworld_single_tile_paint.jsonl`):** -```jsonl -{"action": "start_test", "name": "overworld_single_tile_paint", "description": "Paint single tile and verify"} -{"action": "setup", "rom": "zelda3.sfc", "window": "Overworld Editor", "map": 0} -{"action": "wait_for_window", "window_name": "Overworld Editor", "timeout_ms": 5000} -{"action": "select_tile", "tile_id": 66, "tile_id_hex": "0x0042"} -{"action": "click_canvas_tile", "canvas_id": "Overworld Canvas", "x": 10, "y": 10} -{"action": "assert_tile", "x": 10, "y": 10, "expected_tile_id": 66} -{"action": "end_test", "status": "pass"} -``` - -**Run a scenario:** -```bash -z3ed agent test replay overworld_single_tile_paint.jsonl --rom zelda3.sfc --grpc localhost:50051 -``` +Refer to the `z3ed` agent guide for details on using commands like `z3ed gui discover`, `z3ed gui click`, and `z3ed agent test replay`. \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 80e111fb..93900392 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -342,7 +342,7 @@ if(YAZE_BUILD_EMU) MACOSX_BUNDLE app/emu/emu.cc app/rom.cc - app/core/platform/app_delegate.mm + app/platform/app_delegate.mm ${YAZE_APP_EMU_SRC} ${YAZE_APP_CORE_SRC} ${YAZE_APP_EDITOR_SRC} @@ -746,18 +746,18 @@ source_group("Application\\Core\\Asar" FILES # Platform-specific files source_group("Application\\Core\\Platform" FILES - app/core/platform/app_delegate.h - app/core/platform/app_delegate.mm - app/core/platform/clipboard.cc - app/core/platform/clipboard.h - app/core/platform/clipboard.mm - app/core/platform/file_dialog.cc - app/core/platform/file_dialog.h - app/core/platform/file_dialog.mm - app/core/platform/font_loader.cc - app/core/platform/font_loader.h - app/core/platform/font_loader.mm - app/core/platform/view_controller.h + app/platform/app_delegate.h + app/platform/app_delegate.mm + app/platform/clipboard.cc + app/platform/clipboard.h + app/platform/clipboard.mm + app/platform/file_dialog.cc + app/platform/file_dialog.h + app/platform/file_dialog.mm + app/platform/font_loader.cc + app/platform/font_loader.h + app/platform/font_loader.mm + app/platform/view_controller.h ) # Editor System diff --git a/src/app/core/controller.cc b/src/app/core/controller.cc index a4ca4d8a..9618042f 100644 --- a/src/app/core/controller.cc +++ b/src/app/core/controller.cc @@ -6,6 +6,8 @@ #include "app/core/window.h" #include "app/editor/editor_manager.h" #include "app/editor/ui/background_renderer.h" +#include "app/gfx/arena.h" // Add include for Arena +#include "app/gfx/backend/sdl2_renderer.h" // Add include for new renderer #include "app/gui/theme_manager.h" #include "app/gui/widgets/widget_id_registry.h" #include "imgui/backends/imgui_impl_sdl2.h" @@ -16,10 +18,21 @@ namespace yaze { namespace core { absl::Status Controller::OnEntry(std::string filename) { - RETURN_IF_ERROR(CreateWindow(window_, SDL_WINDOW_RESIZABLE)); + // Create renderer FIRST + renderer_ = std::make_unique(); + + // Call CreateWindow with our renderer + RETURN_IF_ERROR(CreateWindow(window_, renderer_.get(), SDL_WINDOW_RESIZABLE)); + + // Initialize the graphics Arena with the renderer + gfx::Arena::Get().Initialize(renderer_.get()); + + // Set up audio for emulator editor_manager_.emulator().set_audio_buffer(window_.audio_buffer_.get()); editor_manager_.emulator().set_audio_device_id(window_.audio_device_); - editor_manager_.Initialize(filename); + + // Initialize editor manager with renderer + editor_manager_.Initialize(renderer_.get(), filename); active_ = true; return absl::OkStatus(); } @@ -74,14 +87,20 @@ absl::Status Controller::OnLoad() { } void Controller::DoRender() const { + // Process all pending texture commands. + gfx::Arena::Get().ProcessTextureQueue(renderer_.get()); + ImGui::Render(); - SDL_RenderClear(Renderer::Get().renderer()); + renderer_->Clear(); ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData(), - Renderer::Get().renderer()); - SDL_RenderPresent(Renderer::Get().renderer()); + static_cast(renderer_->GetBackendRenderer())); + renderer_->Present(); } -void Controller::OnExit() { PRINT_IF_ERROR(ShutdownWindow(window_)); } +void Controller::OnExit() { + renderer_->Shutdown(); + PRINT_IF_ERROR(ShutdownWindow(window_)); +} } // namespace core } // namespace yaze diff --git a/src/app/core/controller.h b/src/app/core/controller.h index 332fd9ad..9cd0dc76 100644 --- a/src/app/core/controller.h +++ b/src/app/core/controller.h @@ -8,6 +8,7 @@ #include "absl/status/status.h" #include "app/core/window.h" #include "app/editor/editor_manager.h" +#include "app/gfx/backend/irenderer.h" int main(int argc, char** argv); @@ -36,6 +37,7 @@ class Controller { return editor_manager_.overworld(); } auto GetCurrentRom() -> Rom* { return editor_manager_.GetCurrentRom(); } + auto renderer() -> gfx::IRenderer* { return renderer_.get(); } private: friend int ::main(int argc, char** argv); @@ -43,6 +45,7 @@ class Controller { bool active_ = false; core::Window window_; editor::EditorManager editor_manager_; + std::unique_ptr renderer_; }; } // namespace core diff --git a/src/app/core/core_library.cmake b/src/app/core/core_library.cmake index c6aae590..cff7f1c1 100644 --- a/src/app/core/core_library.cmake +++ b/src/app/core/core_library.cmake @@ -9,18 +9,18 @@ set( if (WIN32 OR MINGW OR (UNIX AND NOT APPLE)) list(APPEND YAZE_APP_CORE_SRC - app/core/platform/font_loader.cc - app/core/platform/asset_loader.cc + app/platform/font_loader.cc + app/platform/asset_loader.cc ) endif() if(APPLE) list(APPEND YAZE_APP_CORE_SRC - app/core/platform/file_dialog.mm - app/core/platform/app_delegate.mm - app/core/platform/font_loader.cc - app/core/platform/font_loader.mm - app/core/platform/asset_loader.cc + app/platform/file_dialog.mm + app/platform/app_delegate.mm + app/platform/font_loader.cc + app/platform/font_loader.mm + app/platform/asset_loader.cc ) find_library(COCOA_LIBRARY Cocoa) diff --git a/src/app/core/service/unified_grpc_server.cc b/src/app/core/service/unified_grpc_server.cc index 47ecd258..a7fae2c8 100644 --- a/src/app/core/service/unified_grpc_server.cc +++ b/src/app/core/service/unified_grpc_server.cc @@ -28,9 +28,9 @@ YazeGRPCServer::~YazeGRPCServer() { absl::Status YazeGRPCServer::Initialize( int port, test::TestManager* test_manager, - app::Rom* rom, - app::net::RomVersionManager* version_mgr, - app::net::ProposalApprovalManager* approval_mgr, + Rom* rom, + net::RomVersionManager* version_mgr, + net::ProposalApprovalManager* approval_mgr, CanvasAutomationServiceImpl* canvas_service) { if (is_running_) { @@ -50,11 +50,11 @@ absl::Status YazeGRPCServer::Initialize( // Create ROM service if rom provided if (config_.enable_rom_service && rom) { - rom_service_ = std::make_unique( + rom_service_ = std::make_unique( rom, version_mgr, approval_mgr); // Configure ROM service - app::net::RomServiceImpl::Config rom_config; + net::RomServiceImpl::Config rom_config; rom_config.require_approval_for_writes = config_.require_approval_for_rom_writes; rom_service_->SetConfig(rom_config); diff --git a/src/app/core/service/unified_grpc_server.h b/src/app/core/service/unified_grpc_server.h index bc7bb245..d1ab4123 100644 --- a/src/app/core/service/unified_grpc_server.h +++ b/src/app/core/service/unified_grpc_server.h @@ -20,13 +20,13 @@ namespace yaze { // Forward declarations class CanvasAutomationServiceImpl; -namespace app { + class Rom; namespace net { class ProposalApprovalManager; class RomServiceImpl; } -} + namespace test { class TestManager; @@ -85,9 +85,9 @@ class YazeGRPCServer { absl::Status Initialize( int port, test::TestManager* test_manager = nullptr, - app::Rom* rom = nullptr, - app::net::RomVersionManager* version_mgr = nullptr, - app::net::ProposalApprovalManager* approval_mgr = nullptr, + Rom* rom = nullptr, + net::RomVersionManager* version_mgr = nullptr, + net::ProposalApprovalManager* approval_mgr = nullptr, CanvasAutomationServiceImpl* canvas_service = nullptr); /** @@ -126,7 +126,7 @@ class YazeGRPCServer { Config config_; std::unique_ptr server_; std::unique_ptr test_harness_service_; - std::unique_ptr rom_service_; + std::unique_ptr rom_service_; std::unique_ptr canvas_service_; // Store as base grpc::Service* to avoid incomplete type issues std::unique_ptr canvas_grpc_service_; diff --git a/src/app/core/window.cc b/src/app/core/window.cc index b8fc941b..b546b86d 100644 --- a/src/app/core/window.cc +++ b/src/app/core/window.cc @@ -4,7 +4,7 @@ #include "absl/status/status.h" #include "absl/strings/str_format.h" -#include "app/core/platform/font_loader.h" +#include "app/platform/font_loader.h" #include "util/sdl_deleter.h" #include "util/log.h" #include "app/gfx/arena.h" @@ -51,7 +51,7 @@ void ImGuiAssertionHandler(const char* expr, const char* file, int line, namespace yaze { namespace core { -absl::Status CreateWindow(Window& window, int flags) { +absl::Status CreateWindow(Window& window, gfx::IRenderer* renderer, int flags) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0) { return absl::InternalError( absl::StrFormat("SDL_Init: %s\n", SDL_GetError())); @@ -72,7 +72,12 @@ absl::Status CreateWindow(Window& window, int flags) { absl::StrFormat("SDL_CreateWindow: %s\n", SDL_GetError())); } - RETURN_IF_ERROR(Renderer::Get().CreateRenderer(window.window_.get())); + // Only initialize renderer if one is provided and not already initialized + if (renderer && !renderer->GetBackendRenderer()) { + if (!renderer->Initialize(window.window_.get())) { + return absl::InternalError("Failed to initialize renderer"); + } + } IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -93,30 +98,37 @@ absl::Status CreateWindow(Window& window, int flags) { test::TestManager::Get().InitializeUITesting(); #endif - ImGui_ImplSDL2_InitForSDLRenderer(window.window_.get(), - Renderer::Get().renderer()); - ImGui_ImplSDLRenderer2_Init(Renderer::Get().renderer()); + // Initialize ImGui backends if renderer is provided + if (renderer) { + SDL_Renderer* sdl_renderer = static_cast(renderer->GetBackendRenderer()); + ImGui_ImplSDL2_InitForSDLRenderer(window.window_.get(), sdl_renderer); + ImGui_ImplSDLRenderer2_Init(sdl_renderer); + } RETURN_IF_ERROR(LoadPackageFonts()); // Apply original YAZE colors as fallback, then try to load theme system gui::ColorsYaze(); - const int audio_frequency = 48000; - SDL_AudioSpec want, have; - SDL_memset(&want, 0, sizeof(want)); - want.freq = audio_frequency; - want.format = AUDIO_S16; - want.channels = 2; - want.samples = 2048; - want.callback = NULL; // Uses the queue - window.audio_device_ = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); + // Initialize audio if not already initialized if (window.audio_device_ == 0) { - throw std::runtime_error( - absl::StrFormat("Failed to open audio: %s\n", SDL_GetError())); + const int audio_frequency = 48000; + SDL_AudioSpec want, have; + SDL_memset(&want, 0, sizeof(want)); + want.freq = audio_frequency; + want.format = AUDIO_S16; + want.channels = 2; + want.samples = 2048; + want.callback = NULL; // Uses the queue + window.audio_device_ = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); + if (window.audio_device_ == 0) { + LOG_ERROR("Window", "Failed to open audio: %s", SDL_GetError()); + // Don't fail - audio is optional + } else { + window.audio_buffer_ = std::make_shared(audio_frequency / 50 * 4); + SDL_PauseAudioDevice(window.audio_device_, 0); + } } - window.audio_buffer_ = std::make_shared(audio_frequency / 50 * 4); - SDL_PauseAudioDevice(window.audio_device_, 0); return absl::OkStatus(); } @@ -129,12 +141,20 @@ absl::Status ShutdownWindow(Window& window) { #ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE test::TestManager::Get().StopUITesting(); #endif + + // TODO: BAD FIX, SLOW SHUTDOWN TAKES TOO LONG NOW + // CRITICAL FIX: Shutdown graphics arena FIRST + // This ensures all textures are destroyed while renderer is still valid + LOG_INFO("Window", "Shutting down graphics arena..."); + gfx::Arena::Get().Shutdown(); - // Shutdown ImGui implementations + // Shutdown ImGui implementations (after Arena but before context) + LOG_INFO("Window", "Shutting down ImGui implementations..."); ImGui_ImplSDL2_Shutdown(); ImGui_ImplSDLRenderer2_Shutdown(); // Destroy ImGui context + LOG_INFO("Window", "Destroying ImGui context..."); ImGui::DestroyContext(); // NOW destroy test engine context (after ImGui context is destroyed) @@ -142,12 +162,14 @@ absl::Status ShutdownWindow(Window& window) { test::TestManager::Get().DestroyUITestingContext(); #endif - // Shutdown graphics arena BEFORE destroying SDL contexts - gfx::Arena::Get().Shutdown(); - - SDL_DestroyRenderer(Renderer::Get().renderer()); + // Finally destroy window + LOG_INFO("Window", "Destroying window..."); SDL_DestroyWindow(window.window_.get()); + + LOG_INFO("Window", "Shutting down SDL..."); SDL_Quit(); + + LOG_INFO("Window", "Shutdown complete"); return absl::OkStatus(); } diff --git a/src/app/core/window.h b/src/app/core/window.h index ec4d41e7..75207800 100644 --- a/src/app/core/window.h +++ b/src/app/core/window.h @@ -9,6 +9,7 @@ #include "absl/strings/str_format.h" #include "util/sdl_deleter.h" #include "app/gfx/bitmap.h" +#include "app/gfx/backend/irenderer.h" namespace yaze { namespace core { @@ -20,136 +21,13 @@ struct Window { bool active_ = true; }; -absl::Status CreateWindow(Window &window, int flags); +// Legacy CreateWindow (deprecated - use Controller::OnEntry instead) +// Kept for backward compatibility with test code +absl::Status CreateWindow(Window& window, gfx::IRenderer* renderer = nullptr, + int flags = SDL_WINDOW_RESIZABLE); absl::Status HandleEvents(Window &window); absl::Status ShutdownWindow(Window &window); -/** - * @class Renderer - * @brief The Renderer class represents the renderer for the Yaze application. - * - * This class is a singleton that provides functionality for creating and - * rendering bitmaps to the screen. It also includes methods for updating - * bitmaps on the screen. - * - * IMPORTANT: This class MUST be used only on the main thread because: - * 1. SDL_Renderer operations are not thread-safe - * 2. OpenGL/DirectX contexts are bound to the creating thread - * 3. Texture creation and rendering must happen on the main UI thread - * - * For performance optimization during ROM loading: - * - Use deferred texture creation (CreateBitmapWithoutTexture) for bulk operations - * - Batch texture creation operations when possible - * - Consider background processing of bitmap data before texture creation - */ -class Renderer { - public: - static Renderer &Get() { - static Renderer instance; - return instance; - } - - /** - * @brief Initialize the SDL renderer on the main thread - * - * This MUST be called from the main thread as SDL renderer operations - * are not thread-safe and require the OpenGL/DirectX context to be bound - * to the calling thread. - */ - absl::Status CreateRenderer(SDL_Window *window) { - renderer_ = std::unique_ptr( - SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED)); - if (renderer_ == nullptr) { - return absl::InternalError( - absl::StrFormat("SDL_CreateRenderer: %s\n", SDL_GetError())); - } - SDL_SetRenderDrawBlendMode(renderer_.get(), SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer_.get(), 0x00, 0x00, 0x00, 0x00); - return absl::OkStatus(); - } - - auto renderer() -> SDL_Renderer * { return renderer_.get(); } - - /** - * @brief Create texture for bitmap on main thread - * - * This operation blocks the main thread and should be used sparingly - * during bulk loading operations. Consider using CreateBitmapWithoutTexture - * followed by batch texture creation. - */ - void RenderBitmap(gfx::Bitmap *bitmap) { - bitmap->CreateTexture(renderer_.get()); - } - - /** - * @brief Update existing texture on main thread - */ - void UpdateBitmap(gfx::Bitmap *bitmap) { - bitmap->UpdateTexture(renderer_.get()); - } - - /** - * @brief Create bitmap and immediately create texture (blocking operation) - * - * This is the original method that blocks during texture creation. - * For performance during ROM loading, consider using CreateBitmapWithoutTexture - * and deferring texture creation until needed. - */ - void CreateAndRenderBitmap(int width, int height, int depth, - const std::vector &data, - gfx::Bitmap &bitmap, gfx::SnesPalette &palette) { - bitmap.Create(width, height, depth, data); - bitmap.SetPalette(palette); - RenderBitmap(&bitmap); - } - - /** - * @brief Create bitmap without creating texture (non-blocking) - * - * This method prepares the bitmap data and surface but doesn't create - * the GPU texture, allowing for faster bulk operations during ROM loading. - * Texture creation can be deferred until the bitmap is actually needed - * for rendering. - */ - void CreateBitmapWithoutTexture(int width, int height, int depth, - const std::vector &data, - gfx::Bitmap &bitmap, gfx::SnesPalette &palette) { - bitmap.Create(width, height, depth, data); - bitmap.SetPalette(palette); - // Note: No RenderBitmap call - texture creation is deferred - } - - /** - * @brief Batch create textures for multiple bitmaps - * - * This method can be used to efficiently create textures for multiple - * bitmaps that have already been prepared with CreateBitmapWithoutTexture. - * Useful for deferred texture creation during ROM loading. - */ - void BatchCreateTextures(std::vector &bitmaps) { - for (auto* bitmap : bitmaps) { - if (bitmap && !bitmap->texture()) { - bitmap->CreateTexture(renderer_.get()); - } - } - } - - void Clear() { - SDL_SetRenderDrawColor(renderer_.get(), 0x00, 0x00, 0x00, 0x00); - SDL_RenderClear(renderer_.get()); - } - - void Present() { SDL_RenderPresent(renderer_.get()); } - - private: - Renderer() = default; - - std::unique_ptr renderer_; - - Renderer(const Renderer &) = delete; - Renderer &operator=(const Renderer &) = delete; -}; - } // namespace core } // namespace yaze #endif // YAZE_CORE_WINDOW_H_ \ No newline at end of file diff --git a/src/app/editor/agent/agent_editor.cc b/src/app/editor/agent/agent_editor.cc index 3f5d3c50..d755b376 100644 --- a/src/app/editor/agent/agent_editor.cc +++ b/src/app/editor/agent/agent_editor.cc @@ -7,7 +7,7 @@ #include "absl/strings/match.h" #include "absl/strings/str_format.h" #include "absl/time/clock.h" -#include "app/core/platform/asset_loader.h" +#include "app/platform/asset_loader.h" #include "app/editor/agent/agent_chat_widget.h" #include "app/editor/agent/agent_collaboration_coordinator.h" #include "app/editor/system/proposal_drawer.h" @@ -638,7 +638,7 @@ void AgentEditor::DrawPromptEditorPanel() { // Load prompt file if not initialized if (!prompt_editor_initialized_ && prompt_editor_) { std::string asset_path = "agent/" + active_prompt_file_; - auto content_result = core::AssetLoader::LoadTextFile(asset_path); + auto content_result = AssetLoader::LoadTextFile(asset_path); if (content_result.ok()) { prompt_editor_->SetText(*content_result); @@ -923,7 +923,7 @@ void AgentEditor::DrawCommonTilesEditor() { // Load/Save buttons if (ImGui::Button(ICON_MD_FOLDER_OPEN " Load", ImVec2(100, 0))) { - auto content = core::AssetLoader::LoadTextFile("agent/common_tiles.txt"); + auto content = AssetLoader::LoadTextFile("agent/common_tiles.txt"); if (content.ok()) { common_tiles_editor_->SetText(*content); common_tiles_initialized_ = true; @@ -954,7 +954,7 @@ void AgentEditor::DrawCommonTilesEditor() { // Load if not initialized if (!common_tiles_initialized_ && common_tiles_editor_) { - auto content = core::AssetLoader::LoadTextFile("agent/common_tiles.txt"); + auto content = AssetLoader::LoadTextFile("agent/common_tiles.txt"); if (content.ok()) { common_tiles_editor_->SetText(*content); } else { @@ -1010,7 +1010,7 @@ void AgentEditor::DrawNewPromptCreator() { ImGui::Text("Start from template:"); if (ImGui::Button(ICON_MD_FILE_COPY " v1 (Basic)", ImVec2(-1, 0))) { - auto content = core::AssetLoader::LoadTextFile("agent/system_prompt.txt"); + auto content = AssetLoader::LoadTextFile("agent/system_prompt.txt"); if (content.ok() && prompt_editor_) { prompt_editor_->SetText(*content); if (toast_manager_) { @@ -1021,7 +1021,7 @@ void AgentEditor::DrawNewPromptCreator() { if (ImGui::Button(ICON_MD_FILE_COPY " v2 (Enhanced)", ImVec2(-1, 0))) { auto content = - core::AssetLoader::LoadTextFile("agent/system_prompt_v2.txt"); + AssetLoader::LoadTextFile("agent/system_prompt_v2.txt"); if (content.ok() && prompt_editor_) { prompt_editor_->SetText(*content); if (toast_manager_) { @@ -1032,7 +1032,7 @@ void AgentEditor::DrawNewPromptCreator() { if (ImGui::Button(ICON_MD_FILE_COPY " v3 (Proactive)", ImVec2(-1, 0))) { auto content = - core::AssetLoader::LoadTextFile("agent/system_prompt_v3.txt"); + AssetLoader::LoadTextFile("agent/system_prompt_v3.txt"); if (content.ok() && prompt_editor_) { prompt_editor_->SetText(*content); if (toast_manager_) { diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.cc b/src/app/editor/dungeon/dungeon_canvas_viewer.cc index 22b57b66..624bf02f 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.cc +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.cc @@ -1,14 +1,10 @@ #include "dungeon_canvas_viewer.h" #include "absl/strings/str_format.h" -#include "app/core/window.h" #include "app/gfx/arena.h" #include "app/gfx/snes_palette.h" -#include "app/gui/canvas.h" #include "app/gui/input.h" #include "app/rom.h" -#include "util/log.h" -#include "app/zelda3/dungeon/object_drawer.h" #include "app/zelda3/dungeon/object_renderer.h" #include "app/zelda3/dungeon/room.h" #include "app/zelda3/sprite/sprite.h" @@ -148,6 +144,7 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) { object_interaction_.HandleCanvasMouseInput(); object_interaction_.CheckForObjectSelection(); object_interaction_.DrawSelectBox(); + object_interaction_.DrawSelectionHighlights(); // Draw selection highlights on top } } @@ -195,7 +192,9 @@ void DungeonCanvasViewer::RenderObjectInCanvas(const zelda3::RoomObject &object, // Ensure the bitmap is valid and has content if (object_bitmap.width() > 0 && object_bitmap.height() > 0) { object_bitmap.SetPalette(palette); - core::Renderer::Get().RenderBitmap(&object_bitmap); + // Queue texture creation for the object bitmap via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &object_bitmap); canvas_.DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255); return; } @@ -656,7 +655,10 @@ absl::Status DungeonCanvasViewer::UpdateRoomBackgroundLayers(int room_id) { if (current_palette_id_ < current_palette_group_.size()) { gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( current_palette_group_[current_palette_id_], 0); - core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, + &gfx::Arena::Get().gfx_sheets()[block]); } } } @@ -671,7 +673,10 @@ absl::Status DungeonCanvasViewer::UpdateRoomBackgroundLayers(int room_id) { if (block >= 0 && block < gfx::Arena::Get().gfx_sheets().size()) { gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( sprites_aux1_pal_group[current_palette_id_], 0); - core::Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, + &gfx::Arena::Get().gfx_sheets()[block]); } } } @@ -691,14 +696,9 @@ void DungeonCanvasViewer::RenderRoomBackgroundLayers(int room_id) { if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0 && bg1_bitmap.height() > 0) { if (!bg1_bitmap.texture()) { - core::Renderer::Get().RenderBitmap(&bg1_bitmap); - } - - // DEBUG: Check SDL texture format - Uint32 format; - int access, w, h; - if (SDL_QueryTexture(bg1_bitmap.texture(), &format, &access, &w, &h) == 0) { - const char* format_name_cstr = SDL_GetPixelFormatName(format); + // Queue texture creation for background layer 1 via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &bg1_bitmap); } canvas_.DrawBitmap(bg1_bitmap, 0, 0, 1.0f, 255); @@ -706,7 +706,9 @@ void DungeonCanvasViewer::RenderRoomBackgroundLayers(int room_id) { if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0 && bg2_bitmap.height() > 0) { if (!bg2_bitmap.texture()) { - core::Renderer::Get().RenderBitmap(&bg2_bitmap); + // Queue texture creation for background layer 2 via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &bg2_bitmap); } canvas_.DrawBitmap(bg2_bitmap, 0, 0, 1.0f, 200); } diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.h b/src/app/editor/dungeon/dungeon_canvas_viewer.h index 177adf5f..4e1b77e4 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.h +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.h @@ -101,7 +101,7 @@ class DungeonCanvasViewer { int current_active_room_tab_ = 0; // Object interaction state - bool object_interaction_enabled_ = false; + bool object_interaction_enabled_ = true; // Palette data uint64_t current_palette_group_id_ = 0; diff --git a/src/app/editor/dungeon/dungeon_editor.cc b/src/app/editor/dungeon/dungeon_editor.cc index 93c1a873..43dedf7f 100644 --- a/src/app/editor/dungeon/dungeon_editor.cc +++ b/src/app/editor/dungeon/dungeon_editor.cc @@ -18,8 +18,6 @@ namespace yaze::editor { -using core::Renderer; - using ImGui::BeginTabBar; using ImGui::BeginTabItem; using ImGui::BeginTable; @@ -185,11 +183,15 @@ absl::Status DungeonEditor::Save() { } absl::Status DungeonEditor::RefreshGraphics() { + // Update graphics sheet textures via Arena's deferred texture queue std::for_each_n( rooms_[current_room_id_].blocks().begin(), 8, [this](int block) { gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( current_palette_group_[current_palette_id_], 0); - Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + // Queue texture update for the modified graphics sheet + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, + &gfx::Arena::Get().gfx_sheets()[block]); }); auto sprites_aux1_pal_group = rom()->palette_group().sprites_aux1; @@ -198,7 +200,10 @@ absl::Status DungeonEditor::RefreshGraphics() { [this, &sprites_aux1_pal_group](int block) { gfx::Arena::Get().gfx_sheets()[block].SetPaletteWithTransparent( sprites_aux1_pal_group[current_palette_id_], 0); - Renderer::Get().UpdateBitmap(&gfx::Arena::Get().gfx_sheets()[block]); + // Queue texture update for the modified graphics sheet + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, + &gfx::Arena::Get().gfx_sheets()[block]); }); return absl::OkStatus(); } diff --git a/src/app/editor/dungeon/dungeon_editor_v2.cc b/src/app/editor/dungeon/dungeon_editor_v2.cc index b2d1167a..f911c910 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.cc +++ b/src/app/editor/dungeon/dungeon_editor_v2.cc @@ -15,7 +15,9 @@ namespace yaze::editor { // No table layout needed - all cards are independent -void DungeonEditorV2::Initialize() { +void DungeonEditorV2::Initialize(gfx::IRenderer* renderer, Rom* rom) { + renderer_ = renderer; + rom_ = rom; // Don't initialize emulator preview yet - ROM might not be loaded // Will be initialized in Load() instead @@ -24,6 +26,8 @@ void DungeonEditorV2::Initialize() { room_window_class_.DockingAllowUnclassed = false; // Room windows dock together } +void DungeonEditorV2::Initialize() {} + absl::Status DungeonEditorV2::Load() { if (!rom_ || !rom_->is_loaded()) { return absl::FailedPreconditionError("ROM not loaded"); @@ -55,13 +59,13 @@ absl::Status DungeonEditorV2::Load() { object_selector_.set_rooms(&rooms_); // NOW initialize emulator preview with loaded ROM - object_emulator_preview_.Initialize(rom_); + object_emulator_preview_.Initialize(renderer_, rom_); // Initialize palette editor with loaded ROM palette_editor_.Initialize(rom_); // Initialize unified object editor card - object_editor_card_ = std::make_unique(rom_, &canvas_viewer_); + object_editor_card_ = std::make_unique(renderer_, rom_, &canvas_viewer_); // Wire palette changes to trigger room re-renders palette_editor_.SetOnPaletteChanged([this](int /*palette_id*/) { @@ -97,7 +101,8 @@ absl::Status DungeonEditorV2::Update() { "This parent window can be minimized or closed."); // Render all independent cards (these create their own top-level windows) - object_emulator_preview_.Render(); + // NOTE: Emulator preview is now integrated into ObjectEditorCard + // object_emulator_preview_.Render(); // Removed - causing performance issues DrawLayout(); return absl::OkStatus(); @@ -148,11 +153,7 @@ void DungeonEditorV2::DrawToolset() { toolbar.AddSeparator(); - if (toolbar.AddToggle(ICON_MD_CATEGORY, &show_object_selector_, "Toggle Object Selector (Legacy)")) { - // Toggled - } - - if (toolbar.AddToggle(ICON_MD_CONSTRUCTION, &show_object_editor_, "Toggle Object Editor (Unified)")) { + if (toolbar.AddToggle(ICON_MD_CONSTRUCTION, &show_object_editor_, "Toggle Object Editor")) { // Toggled } @@ -186,18 +187,7 @@ void DungeonEditorV2::DrawLayout() { DrawRoomGraphicsCard(); } - // 4. Legacy Object Selector Card (independent, dockable) - if (show_object_selector_) { - gui::EditorCard object_card( - MakeCardTitle("Object Selector").c_str(), - ICON_MD_CATEGORY, &show_object_selector_); - if (object_card.Begin()) { - object_selector_.Draw(); - } - object_card.End(); - } - - // 4b. Unified Object Editor Card (new, combines selector + preview + interaction) + // 4. Unified Object Editor Card if (show_object_editor_ && object_editor_card_) { object_editor_card_->Draw(&show_object_editor_); } @@ -234,11 +224,16 @@ void DungeonEditorV2::DrawLayout() { room_cards_[room_id] = std::make_shared( card_name_str.c_str(), ICON_MD_GRID_ON, &open); room_cards_[room_id]->SetDefaultSize(700, 600); + + // Set default position for first room to be docked with main window + if (active_rooms_.Size == 1) { + room_cards_[room_id]->SetPosition(gui::EditorCard::Position::Floating); + } } auto& room_card = room_cards_[room_id]; - // Use docking class to make room cards dock together + // CRITICAL: Use docking class BEFORE Begin() to make rooms tab together ImGui::SetNextWindowClass(&room_window_class_); if (room_card->Begin(&open)) { @@ -500,15 +495,15 @@ void DungeonEditorV2::DrawRoomMatrixCard() { MakeCardTitle("Room Matrix").c_str(), ICON_MD_GRID_VIEW, &show_room_matrix_); - matrix_card.SetDefaultSize(520, 620); + matrix_card.SetDefaultSize(440, 520); if (matrix_card.Begin()) { - // 16 wide x 19 tall = 304 cells (295 rooms + 9 empty) + // 16 wide x 19 tall = 304 cells (296 rooms + 8 empty) constexpr int kRoomsPerRow = 16; constexpr int kRoomsPerCol = 19; constexpr int kTotalRooms = 0x128; // 296 rooms (0x00-0x127) - constexpr float kRoomCellSize = 28.0f; // Compact cells - constexpr float kCellSpacing = 2.0f; + constexpr float kRoomCellSize = 24.0f; // Smaller cells like ZScream + constexpr float kCellSpacing = 1.0f; // Tighter spacing ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); @@ -527,30 +522,34 @@ void DungeonEditorV2::DrawRoomMatrixCard() { cell_min.y + kRoomCellSize); if (is_valid_room) { - // ALWAYS use palette-based color (lazy load if needed) - ImU32 bg_color = IM_COL32(60, 60, 70, 255); // Fallback + // Use simple deterministic color based on room ID (no loading needed) + ImU32 bg_color; - // Try to get palette color - uint8_t palette = 0; - if (room_id < static_cast(rooms_.size())) { - // Lazy load room if needed to get palette - if (!rooms_[room_id].IsLoaded()) { - auto status = room_loader_.LoadRoom(room_id, rooms_[room_id]); - if (status.ok()) { - palette = rooms_[room_id].palette; - } - } else { - palette = rooms_[room_id].palette; - } - - // Create a color variation based on palette ID - float r = 0.3f + (palette % 4) * 0.15f; - float g = 0.3f + ((palette / 4) % 4) * 0.15f; - float b = 0.4f + ((palette / 16) % 2) * 0.2f; - bg_color = IM_COL32( - static_cast(r * 255), - static_cast(g * 255), - static_cast(b * 255), 255); + // Generate color from room ID - much faster than loading + int hue = (room_id * 37) % 360; // Distribute colors across spectrum + int saturation = 40 + (room_id % 3) * 15; + int brightness = 50 + (room_id % 5) * 10; + + // Convert HSV to RGB + float h = hue / 60.0f; + float s = saturation / 100.0f; + float v = brightness / 100.0f; + + int i = static_cast(h); + float f = h - i; + int p = static_cast(v * (1 - s) * 255); + int q = static_cast(v * (1 - s * f) * 255); + int t = static_cast(v * (1 - s * (1 - f)) * 255); + int val = static_cast(v * 255); + + switch (i % 6) { + case 0: bg_color = IM_COL32(val, t, p, 255); break; + case 1: bg_color = IM_COL32(q, val, p, 255); break; + case 2: bg_color = IM_COL32(p, val, t, 255); break; + case 3: bg_color = IM_COL32(p, q, val, 255); break; + case 4: bg_color = IM_COL32(t, p, val, 255); break; + case 5: bg_color = IM_COL32(val, p, q, 255); break; + default: bg_color = IM_COL32(60, 60, 70, 255); break; } // Check if room is currently selected @@ -604,7 +603,7 @@ void DungeonEditorV2::DrawRoomMatrixCard() { OnRoomSelected(room_id); } - // Hover tooltip with room name + // Hover tooltip with room name and status if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); if (room_id < static_cast(std::size(zelda3::kRoomNames))) { @@ -612,9 +611,8 @@ void DungeonEditorV2::DrawRoomMatrixCard() { } else { ImGui::Text("Room %03X", room_id); } - ImGui::Text("Palette: %02X", - room_id < static_cast(rooms_.size()) ? - rooms_[room_id].palette : 0); + ImGui::Text("Status: %s", is_open ? "Open" : is_current ? "Current" : "Closed"); + ImGui::Text("Click to %s", is_open ? "focus" : "open"); ImGui::EndTooltip(); } } else { diff --git a/src/app/editor/dungeon/dungeon_editor_v2.h b/src/app/editor/dungeon/dungeon_editor_v2.h index 7c2cce0d..271065e3 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.h +++ b/src/app/editor/dungeon/dungeon_editor_v2.h @@ -51,8 +51,9 @@ class DungeonEditorV2 : public Editor { } // Editor interface + void Initialize(gfx::IRenderer* renderer, Rom* rom); void Initialize() override; - absl::Status Load() override; + absl::Status Load(); absl::Status Update() override; absl::Status Undo() override { return absl::UnimplementedError("Undo"); } absl::Status Redo() override { return absl::UnimplementedError("Redo"); } @@ -69,7 +70,7 @@ class DungeonEditorV2 : public Editor { room_selector_.set_rom(rom); canvas_viewer_.SetRom(rom); object_selector_.SetRom(rom); - object_emulator_preview_.Initialize(rom); + object_emulator_preview_.Initialize(renderer_, rom); } Rom* rom() const { return rom_; } @@ -86,6 +87,7 @@ class DungeonEditorV2 : public Editor { } private: + gfx::IRenderer* renderer_ = nullptr; // Simple UI layout void DrawLayout(); void DrawRoomTab(int room_id); @@ -117,8 +119,7 @@ class DungeonEditorV2 : public Editor { bool show_room_matrix_ = false; bool show_entrances_list_ = false; bool show_room_graphics_ = false; // Room graphics card - bool show_object_selector_ = true; // Legacy object selector - bool show_object_editor_ = true; // New unified object editor card + bool show_object_editor_ = true; // Unified object editor card bool show_palette_editor_ = true; // Palette management diff --git a/src/app/editor/dungeon/dungeon_object_interaction.cc b/src/app/editor/dungeon/dungeon_object_interaction.cc index 5b6aedc8..92cac6a4 100644 --- a/src/app/editor/dungeon/dungeon_object_interaction.cc +++ b/src/app/editor/dungeon/dungeon_object_interaction.cc @@ -150,25 +150,59 @@ void DungeonObjectInteraction::SelectObjectsInRect() { selected_object_indices_.push_back(i); } } +} + +void DungeonObjectInteraction::DrawSelectionHighlights() { + if (!rooms_ || current_room_id_ < 0 || current_room_id_ >= 296) return; - // Highlight selected objects - if (!selected_object_indices_.empty()) { - for (size_t index : selected_object_indices_) { - if (index < objects.size()) { - const auto& object = objects[index]; - auto [canvas_x, canvas_y] = - RoomToCanvasCoordinates(object.x_, object.y_); - - // Draw selection highlight - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - ImVec2 canvas_pos = canvas_->zero_point(); - ImVec2 obj_start(canvas_pos.x + canvas_x - 2, - canvas_pos.y + canvas_y - 2); - ImVec2 obj_end(canvas_pos.x + canvas_x + 18, - canvas_pos.y + canvas_y + 18); - draw_list->AddRect(obj_start, obj_end, IM_COL32(0, 255, 255, 255), 0.0f, - 0, 2.0f); - } + auto& room = (*rooms_)[current_room_id_]; + const auto& objects = room.GetTileObjects(); + + // Draw highlights for all selected objects + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 canvas_pos = canvas_->zero_point(); + + for (size_t index : selected_object_indices_) { + if (index < objects.size()) { + const auto& object = objects[index]; + auto [canvas_x, canvas_y] = RoomToCanvasCoordinates(object.x_, object.y_); + + // Calculate object size for highlight + int obj_width = 8 + (object.size_ & 0x0F) * 4; + int obj_height = 8 + ((object.size_ >> 4) & 0x0F) * 4; + obj_width = std::min(obj_width, 64); + obj_height = std::min(obj_height, 64); + + // Draw cyan selection highlight + ImVec2 obj_start(canvas_pos.x + canvas_x - 2, + canvas_pos.y + canvas_y - 2); + ImVec2 obj_end(canvas_pos.x + canvas_x + obj_width + 2, + canvas_pos.y + canvas_y + obj_height + 2); + + // Animated selection (pulsing effect) + float pulse = 0.7f + 0.3f * std::sin(static_cast(ImGui::GetTime()) * 4.0f); + draw_list->AddRect(obj_start, obj_end, + IM_COL32(0, static_cast(255 * pulse), 255, 255), + 0.0f, 0, 2.5f); + + // Draw corner handles for selected objects + constexpr float handle_size = 4.0f; + draw_list->AddRectFilled( + ImVec2(obj_start.x - handle_size/2, obj_start.y - handle_size/2), + ImVec2(obj_start.x + handle_size/2, obj_start.y + handle_size/2), + IM_COL32(0, 255, 255, 255)); + draw_list->AddRectFilled( + ImVec2(obj_end.x - handle_size/2, obj_start.y - handle_size/2), + ImVec2(obj_end.x + handle_size/2, obj_start.y + handle_size/2), + IM_COL32(0, 255, 255, 255)); + draw_list->AddRectFilled( + ImVec2(obj_start.x - handle_size/2, obj_end.y - handle_size/2), + ImVec2(obj_start.x + handle_size/2, obj_end.y + handle_size/2), + IM_COL32(0, 255, 255, 255)); + draw_list->AddRectFilled( + ImVec2(obj_end.x - handle_size/2, obj_end.y - handle_size/2), + ImVec2(obj_end.x + handle_size/2, obj_end.y + handle_size/2), + IM_COL32(0, 255, 255, 255)); } } } diff --git a/src/app/editor/dungeon/dungeon_object_interaction.h b/src/app/editor/dungeon/dungeon_object_interaction.h index 0500801c..3c129fcb 100644 --- a/src/app/editor/dungeon/dungeon_object_interaction.h +++ b/src/app/editor/dungeon/dungeon_object_interaction.h @@ -30,6 +30,7 @@ class DungeonObjectInteraction { // Selection rectangle (like OverworldEditor) void DrawObjectSelectRect(); void SelectObjectsInRect(); + void DrawSelectionHighlights(); // Draw highlights for selected objects // Drag and select box functionality void DrawSelectBox(); diff --git a/src/app/editor/dungeon/dungeon_object_selector.cc b/src/app/editor/dungeon/dungeon_object_selector.cc index bc351da4..2e916e0a 100644 --- a/src/app/editor/dungeon/dungeon_object_selector.cc +++ b/src/app/editor/dungeon/dungeon_object_selector.cc @@ -90,7 +90,9 @@ void DungeonObjectSelector::DrawObjectRenderer() { auto preview_bitmap = std::move(preview_result.value()); if (preview_bitmap.width() > 0 && preview_bitmap.height() > 0) { preview_bitmap.SetPalette(preview_palette_); - core::Renderer::Get().RenderBitmap(&preview_bitmap); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &preview_bitmap); object_canvas_.DrawBitmap(preview_bitmap, preview_x, preview_y, 1.0f, 255); } else { // Fallback: Draw primitive shape diff --git a/src/app/editor/dungeon/dungeon_renderer.cc b/src/app/editor/dungeon/dungeon_renderer.cc index 9a008469..7fb60750 100644 --- a/src/app/editor/dungeon/dungeon_renderer.cc +++ b/src/app/editor/dungeon/dungeon_renderer.cc @@ -1,14 +1,10 @@ #include "dungeon_renderer.h" #include "absl/strings/str_format.h" -#include "app/core/window.h" #include "app/gfx/arena.h" -#include "app/gui/color.h" namespace yaze::editor { -using core::Renderer; - void DungeonRenderer::RenderObjectInCanvas(const zelda3::RoomObject& object, const gfx::SnesPalette& palette) { // Validate ROM is loaded @@ -55,7 +51,9 @@ void DungeonRenderer::RenderObjectInCanvas(const zelda3::RoomObject& object, if (object_bitmap.width() > 0 && object_bitmap.height() > 0 && object_bitmap.data() != nullptr) { object_bitmap.SetPalette(palette); - core::Renderer::Get().RenderBitmap(&object_bitmap); + // Queue texture creation for the object bitmap via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &object_bitmap); canvas_->DrawBitmap(object_bitmap, canvas_x, canvas_y, 1.0f, 255); // Cache the successfully rendered bitmap ObjectRenderCache cache_entry; diff --git a/src/app/editor/dungeon/object_editor_card.cc b/src/app/editor/dungeon/object_editor_card.cc index ecd10069..c963e184 100644 --- a/src/app/editor/dungeon/object_editor_card.cc +++ b/src/app/editor/dungeon/object_editor_card.cc @@ -1,15 +1,16 @@ #include "object_editor_card.h" #include "absl/strings/str_format.h" +#include "app/gfx/backend/irenderer.h" #include "app/gui/icons.h" #include "app/gui/ui_helpers.h" #include "imgui/imgui.h" namespace yaze::editor { -ObjectEditorCard::ObjectEditorCard(Rom* rom, DungeonCanvasViewer* canvas_viewer) - : rom_(rom), canvas_viewer_(canvas_viewer), object_selector_(rom) { - emulator_preview_.Initialize(rom); +ObjectEditorCard::ObjectEditorCard(gfx::IRenderer* renderer, Rom* rom, DungeonCanvasViewer* canvas_viewer) + : renderer_(renderer), rom_(rom), canvas_viewer_(canvas_viewer), object_selector_(rom) { + emulator_preview_.Initialize(renderer, rom); } void ObjectEditorCard::Draw(bool* p_open) { @@ -93,7 +94,7 @@ void ObjectEditorCard::DrawObjectSelector() { ImGui::Separator(); // Object list with categories - if (ImGui::BeginChild("##ObjectList", ImVec2(0, -100), true)) { + if (ImGui::BeginChild("##ObjectList", ImVec2(0, 0), true)) { // Floor objects if (ImGui::CollapsingHeader(ICON_MD_GRID_ON " Floor Objects", ImGuiTreeNodeFlags_DefaultOpen)) { @@ -268,8 +269,10 @@ void ObjectEditorCard::DrawObjectPreviewIcon(int object_id, const ImVec2& size) void ObjectEditorCard::DrawSelectedObjectInfo() { ImGui::BeginGroup(); + + // Show current object for placement ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), - ICON_MD_INFO " Current Object:"); + ICON_MD_INFO " Current:"); if (has_preview_object_) { ImGui::SameLine(); @@ -278,14 +281,35 @@ void ObjectEditorCard::DrawSelectedObjectInfo() { ImGui::Text("Layer: %s", preview_object_.layer_ == zelda3::RoomObject::BG1 ? "BG1" : preview_object_.layer_ == zelda3::RoomObject::BG2 ? "BG2" : "BG3"); - ImGui::SameLine(); - ImGui::Text("Mode: %s", - interaction_mode_ == InteractionMode::Place ? "Place" : - interaction_mode_ == InteractionMode::Select ? "Select" : - interaction_mode_ == InteractionMode::Delete ? "Delete" : "None"); } else { ImGui::SameLine(); - ImGui::TextDisabled("None selected"); + ImGui::TextDisabled("None"); + } + + // Show selection count + auto& interaction = canvas_viewer_->object_interaction(); + const auto& selected = interaction.GetSelectedObjectIndices(); + + ImGui::SameLine(); + ImGui::Text("|"); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.4f, 1.0f), + ICON_MD_CHECKLIST " Selected: %zu", selected.size()); + + ImGui::SameLine(); + ImGui::Text("|"); + ImGui::SameLine(); + ImGui::Text("Mode: %s", + interaction_mode_ == InteractionMode::Place ? ICON_MD_ADD_BOX " Place" : + interaction_mode_ == InteractionMode::Select ? ICON_MD_CHECK_BOX " Select" : + interaction_mode_ == InteractionMode::Delete ? ICON_MD_DELETE " Delete" : "None"); + + // Show quick actions for selections + if (!selected.empty()) { + ImGui::SameLine(); + if (ImGui::SmallButton(ICON_MD_CLEAR " Clear")) { + interaction.ClearSelection(); + } } ImGui::EndGroup(); diff --git a/src/app/editor/dungeon/object_editor_card.h b/src/app/editor/dungeon/object_editor_card.h index ad6200d6..f2d93364 100644 --- a/src/app/editor/dungeon/object_editor_card.h +++ b/src/app/editor/dungeon/object_editor_card.h @@ -5,6 +5,7 @@ #include #include "app/editor/dungeon/dungeon_canvas_viewer.h" +#include "app/gfx/backend/irenderer.h" #include "app/gui/canvas.h" #include "app/editor/dungeon/dungeon_object_selector.h" #include "app/gui/editor_layout.h" @@ -27,7 +28,7 @@ namespace editor { */ class ObjectEditorCard { public: - ObjectEditorCard(Rom* rom, DungeonCanvasViewer* canvas_viewer); + ObjectEditorCard(gfx::IRenderer* renderer, Rom* rom, DungeonCanvasViewer* canvas_viewer); // Main update function void Draw(bool* p_open); @@ -59,7 +60,7 @@ class ObjectEditorCard { // UI state int selected_tab_ = 0; - bool show_emulator_preview_ = true; + bool show_emulator_preview_ = false; // Disabled by default for performance bool show_object_list_ = true; bool show_interaction_controls_ = true; @@ -75,6 +76,7 @@ class ObjectEditorCard { // Selected object for placement zelda3::RoomObject preview_object_{0, 0, 0, 0, 0}; bool has_preview_object_ = false; + gfx::IRenderer* renderer_; }; } // namespace editor diff --git a/src/app/editor/editor_manager.cc b/src/app/editor/editor_manager.cc index e82b7593..b83fea71 100644 --- a/src/app/editor/editor_manager.cc +++ b/src/app/editor/editor_manager.cc @@ -230,7 +230,9 @@ constexpr const char* kAssemblyEditorName = ICON_MD_CODE " Assembly Editor"; constexpr const char* kDungeonEditorName = ICON_MD_CASTLE " Dungeon Editor"; constexpr const char* kMusicEditorName = ICON_MD_MUSIC_NOTE " Music Editor"; -void EditorManager::Initialize(const std::string& filename) { +void EditorManager::Initialize(gfx::IRenderer* renderer, const std::string& filename) { + renderer_ = renderer; + // Point to a blank editor set when no ROM is loaded current_editor_set_ = &blank_editor_set_; @@ -2097,6 +2099,8 @@ absl::Status EditorManager::LoadAssets() { current_editor_set_->overworld_editor_.Initialize(); current_editor_set_->message_editor_.Initialize(); + // Initialize the dungeon editor with the renderer + current_editor_set_->dungeon_editor_.Initialize(renderer_, current_rom_); ASSIGN_OR_RETURN(*gfx::Arena::Get().mutable_gfx_sheets(), LoadAllGraphicsData(*current_rom_)); RETURN_IF_ERROR(current_editor_set_->overworld_editor_.Load()); diff --git a/src/app/editor/editor_manager.h b/src/app/editor/editor_manager.h index c66d3c8a..c5ce9d35 100644 --- a/src/app/editor/editor_manager.h +++ b/src/app/editor/editor_manager.h @@ -105,7 +105,7 @@ class EditorManager { context_.popup_manager = popup_manager_.get(); } - void Initialize(const std::string& filename = ""); + void Initialize(gfx::IRenderer* renderer, const std::string& filename = ""); absl::Status Update(); void DrawMenuBar(); @@ -262,6 +262,8 @@ class EditorManager { EditorSet* current_editor_set_ = nullptr; Editor* current_editor_ = nullptr; EditorSet blank_editor_set_{}; + + gfx::IRenderer* renderer_ = nullptr; core::YazeProject current_project_; EditorContext context_; diff --git a/src/app/editor/graphics/graphics_editor.cc b/src/app/editor/graphics/graphics_editor.cc index 5da87b93..fc5d9c04 100644 --- a/src/app/editor/graphics/graphics_editor.cc +++ b/src/app/editor/graphics/graphics_editor.cc @@ -29,8 +29,6 @@ namespace yaze { namespace editor { -using core::Renderer; - using gfx::kPaletteGroupAddressesKeys; using ImGui::Button; using ImGui::InputInt; @@ -45,7 +43,50 @@ constexpr ImGuiTableFlags kGfxEditTableFlags = void GraphicsEditor::Initialize() {} -absl::Status GraphicsEditor::Load() { return absl::OkStatus(); } +absl::Status GraphicsEditor::Load() { + gfx::ScopedTimer timer("GraphicsEditor::Load"); + + // Initialize all graphics sheets with appropriate palettes from ROM + if (rom()->is_loaded()) { + auto& sheets = gfx::Arena::Get().gfx_sheets(); + + // Apply default palettes to all sheets based on common SNES ROM structure + // Sheets 0-112: Use overworld/dungeon palettes + // Sheets 113-127: Use sprite palettes + // Sheets 128-222: Use auxiliary/menu palettes + + for (int i = 0; i < kNumGfxSheets; i++) { + if (sheets[i].is_active() && sheets[i].surface()) { + // Determine which palette group to use based on sheet index + gfx::SnesPalette sheet_palette; + + if (i < 113) { + // Overworld/Dungeon graphics - use main palette + auto palette_group = rom()->palette_group().dungeon_main; + sheet_palette = palette_group[0]; // Use first palette as default + } else if (i < 128) { + // Sprite graphics - use sprite palettes + auto palette_group = rom()->palette_group().sprites_aux1; + sheet_palette = palette_group[0]; + } else { + // Auxiliary graphics - use HUD/menu palettes + sheet_palette = rom()->palette_group().hud.palette(0); + } + + // Apply palette and queue texture creation + sheets[i].SetPalette(sheet_palette); + + // Queue texture creation if not already created + if (!sheets[i].texture()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &sheets[i]); + } + } + } + } + + return absl::OkStatus(); +} absl::Status GraphicsEditor::Update() { DrawToolset(); @@ -273,36 +314,43 @@ absl::Status GraphicsEditor::UpdateGfxSheetList() { graphics_bin_canvas_.DrawBackground(ImVec2(0x100 + 1, 0x40 + 1)); graphics_bin_canvas_.DrawContextMenu(); if (value.is_active()) { - auto texture = value.texture(); - graphics_bin_canvas_.draw_list()->AddImage( - (ImTextureID)(intptr_t)texture, - ImVec2(graphics_bin_canvas_.zero_point().x + 2, - graphics_bin_canvas_.zero_point().y + 2), - ImVec2(graphics_bin_canvas_.zero_point().x + - value.width() * sheet_scale_, - graphics_bin_canvas_.zero_point().y + - value.height() * sheet_scale_)); - - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - current_sheet_ = key; - open_sheets_.insert(key); + // Ensure texture exists for active sheets + if (!value.texture() && value.surface()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &value); } + + auto texture = value.texture(); + if (texture) { + graphics_bin_canvas_.draw_list()->AddImage( + (ImTextureID)(intptr_t)texture, + ImVec2(graphics_bin_canvas_.zero_point().x + 2, + graphics_bin_canvas_.zero_point().y + 2), + ImVec2(graphics_bin_canvas_.zero_point().x + + value.width() * sheet_scale_, + graphics_bin_canvas_.zero_point().y + + value.height() * sheet_scale_)); - // Add a slightly transparent rectangle behind the text - ImVec2 text_pos(graphics_bin_canvas_.zero_point().x + 2, - graphics_bin_canvas_.zero_point().y + 2); - ImVec2 text_size = - ImGui::CalcTextSize(absl::StrFormat("%02X", key).c_str()); - ImVec2 rent_min(text_pos.x, text_pos.y); - ImVec2 rent_max(text_pos.x + text_size.x, text_pos.y + text_size.y); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + current_sheet_ = key; + open_sheets_.insert(key); + } - graphics_bin_canvas_.draw_list()->AddRectFilled(rent_min, rent_max, - IM_COL32(0, 125, 0, 128)); + // Add a slightly transparent rectangle behind the text + ImVec2 text_pos(graphics_bin_canvas_.zero_point().x + 2, + graphics_bin_canvas_.zero_point().y + 2); + ImVec2 text_size = + ImGui::CalcTextSize(absl::StrFormat("%02X", key).c_str()); + ImVec2 rect_min(text_pos.x, text_pos.y); + ImVec2 rect_max(text_pos.x + text_size.x, text_pos.y + text_size.y); - graphics_bin_canvas_.draw_list()->AddText( - text_pos, IM_COL32(125, 255, 125, 255), - absl::StrFormat("%02X", key).c_str()); + graphics_bin_canvas_.draw_list()->AddRectFilled(rect_min, rect_max, + IM_COL32(0, 125, 0, 128)); + graphics_bin_canvas_.draw_list()->AddText( + text_pos, IM_COL32(125, 255, 125, 255), + absl::StrFormat("%02X", key).c_str()); + } key++; } graphics_bin_canvas_.DrawGrid(16.0f); @@ -362,13 +410,19 @@ absl::Status GraphicsEditor::UpdateGfxTabView() { current_sheet_canvas_.DrawTileOnBitmap(tile_size_, ¤t_bitmap, current_color_); // Use batch operations for texture updates - current_bitmap.QueueTextureUpdate(core::Renderer::Get().renderer()); + // current_bitmap.QueueTextureUpdate(core::Renderer::Get().renderer()); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_bitmap); }; current_sheet_canvas_.UpdateColorPainter( - gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id), + nullptr, gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id), current_color_, draw_tile_event, tile_size_, current_scale_); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, + &gfx::Arena::Get().mutable_gfx_sheets()->at(sheet_id)); + ImGui::EndChild(); ImGui::EndTabItem(); } @@ -399,7 +453,7 @@ absl::Status GraphicsEditor::UpdateGfxTabView() { current_sheet_ = id; // ImVec2(0x100, 0x40), current_sheet_canvas_.UpdateColorPainter( - gfx::Arena::Get().mutable_gfx_sheets()->at(id), current_color_, + nullptr, gfx::Arena::Get().mutable_gfx_sheets()->at(id), current_color_, [&]() { }, @@ -415,9 +469,6 @@ absl::Status GraphicsEditor::UpdateGfxTabView() { } } - // Process all queued texture updates at once - gfx::Arena::Get().ProcessBatchTextureUpdates(); - return absl::OkStatus(); } @@ -426,24 +477,73 @@ absl::Status GraphicsEditor::UpdatePaletteColumn() { auto palette_group = *rom()->palette_group().get_group( kPaletteGroupAddressesKeys[edit_palette_group_name_index_]); auto palette = palette_group.palette(edit_palette_index_); - gui::TextWithSeparators("ROM Palette"); - ImGui::SetNextItemWidth(100.f); + gui::TextWithSeparators("ROM Palette Management"); + + // Quick palette presets for common SNES graphics types + ImGui::Text("Quick Presets:"); + if (ImGui::Button("Overworld")) { + edit_palette_group_name_index_ = 0; // Dungeon Main + edit_palette_index_ = 0; + refresh_graphics_ = true; + } + ImGui::SameLine(); + if (ImGui::Button("Dungeon")) { + edit_palette_group_name_index_ = 0; // Dungeon Main + edit_palette_index_ = 1; + refresh_graphics_ = true; + } + ImGui::SameLine(); + if (ImGui::Button("Sprites")) { + edit_palette_group_name_index_ = 4; // Sprites Aux1 + edit_palette_index_ = 0; + refresh_graphics_ = true; + } + ImGui::Separator(); + + // Apply current palette to current sheet + if (ImGui::Button("Apply to Current Sheet") && !open_sheets_.empty()) { + refresh_graphics_ = true; + } + ImGui::SameLine(); + if (ImGui::Button("Apply to All Sheets")) { + // Apply current palette to all active sheets + for (int i = 0; i < kNumGfxSheets; i++) { + auto& sheet = gfx::Arena::Get().mutable_gfx_sheets()->data()[i]; + if (sheet.is_active() && sheet.surface()) { + sheet.SetPaletteWithTransparent(palette, edit_palette_sub_index_); + if (sheet.texture()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &sheet); + } + } + } + } + ImGui::Separator(); + + ImGui::SetNextItemWidth(150.f); ImGui::Combo("Palette Group", (int*)&edit_palette_group_name_index_, kPaletteGroupAddressesKeys, IM_ARRAYSIZE(kPaletteGroupAddressesKeys)); ImGui::SetNextItemWidth(100.f); - gui::InputHex("Palette Group Index", &edit_palette_index_); + gui::InputHex("Palette Index", &edit_palette_index_); + ImGui::SetNextItemWidth(100.f); + gui::InputHex("Sub-Palette", &edit_palette_sub_index_); gui::SelectablePalettePipeline(edit_palette_sub_index_, refresh_graphics_, palette); if (refresh_graphics_ && !open_sheets_.empty()) { - gfx::Arena::Get() - .mutable_gfx_sheets() - ->data()[current_sheet_] - .SetPaletteWithTransparent(palette, edit_palette_sub_index_); - Renderer::Get().UpdateBitmap( - &gfx::Arena::Get().mutable_gfx_sheets()->data()[current_sheet_]); + auto& current = gfx::Arena::Get().mutable_gfx_sheets()->data()[current_sheet_]; + if (current.is_active() && current.surface()) { + current.SetPaletteWithTransparent(palette, edit_palette_sub_index_); + if (current.texture()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t); + } else { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, ¤t); + } + } refresh_graphics_ = false; } } @@ -583,7 +683,8 @@ absl::Status GraphicsEditor::DrawCgxImport() { cgx_bitmap_.Create(0x80, 0x200, 8, decoded_cgx_); if (col_file_) { cgx_bitmap_.SetPalette(decoded_col_); - Renderer::Get().RenderBitmap(&cgx_bitmap_); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &cgx_bitmap_); } } @@ -613,7 +714,8 @@ absl::Status GraphicsEditor::DrawScrImport() { scr_bitmap_.Create(0x100, 0x100, 8, decoded_scr_data_); if (scr_loaded_) { scr_bitmap_.SetPalette(decoded_col_); - Renderer::Get().RenderBitmap(&scr_bitmap_); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &scr_bitmap_); } } @@ -815,7 +917,8 @@ absl::Status GraphicsEditor::DecompressImportData(int size) { } } - Renderer::Get().RenderBitmap(&bin_bitmap_); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &bin_bitmap_); gfx_loaded_ = true; return absl::OkStatus(); @@ -844,7 +947,8 @@ absl::Status GraphicsEditor::DecompressSuperDonkey() { gfx_sheets_[i].SetPalette(z3_rom_palette_); } - Renderer::Get().RenderBitmap(&gfx_sheets_[i]); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &gfx_sheets_[i]); i++; } @@ -868,7 +972,8 @@ absl::Status GraphicsEditor::DecompressSuperDonkey() { gfx_sheets_[i].SetPalette(z3_rom_palette_); } - Renderer::Get().RenderBitmap(&gfx_sheets_[i]); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &gfx_sheets_[i]); i++; } super_donkey_ = true; diff --git a/src/app/editor/graphics/screen_editor.cc b/src/app/editor/graphics/screen_editor.cc index 918fd529..dc154a23 100644 --- a/src/app/editor/graphics/screen_editor.cc +++ b/src/app/editor/graphics/screen_editor.cc @@ -26,7 +26,6 @@ namespace yaze { namespace editor { -using core::Renderer; constexpr uint32_t kRedPen = 0xFF0000FF; @@ -56,7 +55,9 @@ absl::Status ScreenEditor::Load() { sheets_[i].Get8x8Tile(tile_index, x, y, tile_data, tile_data_offset); tile8_individual_.emplace_back(gfx::Bitmap(8, 8, 4, tile_data)); tile8_individual_.back().SetPalette(*rom()->mutable_dungeon_palette(3)); - Renderer::Get().RenderBitmap(&tile8_individual_.back()); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &tile8_individual_.back()); } tile_data_offset = 0; } @@ -249,15 +250,17 @@ void ScreenEditor::DrawDungeonMapScreen(int i) { int tile16_id = tile_ids_to_render[idx]; ImVec2 pos = tile_positions[idx]; - gfx::RenderTile16(tile16_blockset_, tile16_id); + gfx::RenderTile16(nullptr, tile16_blockset_, tile16_id); // Get tile from cache after rendering auto* cached_tile = tile16_blockset_.tile_cache.GetTile(tile16_id); if (cached_tile && cached_tile->is_active()) { // Ensure the cached tile has a valid texture if (!cached_tile->texture()) { - core::Renderer::Get().RenderBitmap(cached_tile); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, cached_tile); } - screen_canvas_.DrawBitmap(*cached_tile, pos.x, pos.y, 4.0F); + screen_canvas_.DrawBitmap(*cached_tile, pos.x, pos.y, 4.0F, 255); } } @@ -382,12 +385,12 @@ void ScreenEditor::DrawDungeonMapsRoomGfx() { (tilesheet_canvas_.points().front().y / 32) * 16; // Render selected tile16 and cache tile metadata - gfx::RenderTile16(tile16_blockset_, selected_tile16_); + gfx::RenderTile16(nullptr, tile16_blockset_, selected_tile16_); std::ranges::copy(tile16_blockset_.tile_info[selected_tile16_], current_tile16_info.begin()); } // Use direct bitmap rendering for tilesheet - tilesheet_canvas_.DrawBitmap(tile16_blockset_.atlas, 1, 1, 2.0F); + tilesheet_canvas_.DrawBitmap(tile16_blockset_.atlas, 1, 1, 2.0F, 255); tilesheet_canvas_.DrawGrid(32.f); tilesheet_canvas_.DrawOverlay(); @@ -408,16 +411,18 @@ void ScreenEditor::DrawDungeonMapsRoomGfx() { current_tile16_info[0], current_tile16_info[1], current_tile16_info[2], current_tile16_info[3], 212, selected_tile16_); - gfx::UpdateTile16(tile16_blockset_, selected_tile16_); + gfx::UpdateTile16(nullptr, tile16_blockset_, selected_tile16_); } // Get selected tile from cache auto* selected_tile = tile16_blockset_.tile_cache.GetTile(selected_tile16_); if (selected_tile && selected_tile->is_active()) { // Ensure the selected tile has a valid texture if (!selected_tile->texture()) { - core::Renderer::Get().RenderBitmap(selected_tile); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, selected_tile); } - current_tile_canvas_.DrawBitmap(*selected_tile, 2, 4.0f); + current_tile_canvas_.DrawBitmap(*selected_tile, 2, 2, 4.0f, 255); } current_tile_canvas_.DrawGrid(16.f); current_tile_canvas_.DrawOverlay(); @@ -434,7 +439,7 @@ void ScreenEditor::DrawDungeonMapsRoomGfx() { current_tile16_info[0], current_tile16_info[1], current_tile16_info[2], current_tile16_info[3], 212, selected_tile16_); - gfx::UpdateTile16(tile16_blockset_, selected_tile16_); + gfx::UpdateTile16(nullptr, tile16_blockset_, selected_tile16_); } } ImGui::EndChild(); @@ -543,7 +548,9 @@ void ScreenEditor::LoadBinaryGfx() { converted_bin.begin() + ((i + 1) * 0x1000)); sheets_.emplace(i, gfx::Bitmap(128, 32, 8, gfx_sheets[i])); sheets_[i].SetPalette(*rom()->mutable_dungeon_palette(3)); - Renderer::Get().RenderBitmap(&sheets_[i]); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &sheets_[i]); } binary_gfx_loaded_ = true; } else { diff --git a/src/app/editor/message/message_editor.cc b/src/app/editor/message/message_editor.cc index 725fa358..004e26cd 100644 --- a/src/app/editor/message/message_editor.cc +++ b/src/app/editor/message/message_editor.cc @@ -6,9 +6,9 @@ #include "absl/status/status.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" +#include "app/gfx/arena.h" #include "app/gfx/performance_profiler.h" #include "util/file_util.h" -#include "app/core/window.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" @@ -38,7 +38,6 @@ std::string DisplayTextOverflowError(int pos, bool bank) { } } // namespace -using core::Renderer; using ImGui::BeginChild; using ImGui::BeginTable; @@ -74,11 +73,12 @@ void MessageEditor::Initialize() { } message_preview_.font_gfx16_data_ = gfx::SnesTo8bppSheet(raw_font_gfx_data_, /*bpp=*/2, /*num_sheets=*/2); - Renderer::Get().CreateAndRenderBitmap( - kFontGfxMessageSize, kFontGfxMessageSize, kFontGfxMessageDepth, - message_preview_.font_gfx16_data_, font_gfx_bitmap_, - font_preview_colors_); - *font_gfx_bitmap_.mutable_palette() = font_preview_colors_; + // Create bitmap and queue texture creation + font_gfx_bitmap_.Create(kFontGfxMessageSize, kFontGfxMessageSize, + kFontGfxMessageDepth, message_preview_.font_gfx16_data_); + font_gfx_bitmap_.SetPalette(font_preview_colors_); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &font_gfx_bitmap_); *current_font_gfx16_bitmap_.mutable_palette() = font_preview_colors_; auto load_font = LoadFontGraphics(*rom()); @@ -330,12 +330,16 @@ void MessageEditor::DrawMessagePreview() { if (current_font_gfx16_bitmap_.is_active()) { current_font_gfx16_bitmap_.mutable_data() = message_preview_.current_preview_data_; - Renderer::Get().UpdateBitmap(¤t_font_gfx16_bitmap_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_font_gfx16_bitmap_); } else { - Renderer::Get().CreateAndRenderBitmap( - kCurrentMessageWidth, kCurrentMessageHeight, 172, - message_preview_.current_preview_data_, current_font_gfx16_bitmap_, - font_preview_colors_); + // Create bitmap and queue texture creation + current_font_gfx16_bitmap_.Create(kCurrentMessageWidth, kCurrentMessageHeight, + 172, message_preview_.current_preview_data_); + current_font_gfx16_bitmap_.SetPalette(font_preview_colors_); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, ¤t_font_gfx16_bitmap_); } } diff --git a/src/app/editor/overworld/overworld_editor.cc b/src/app/editor/overworld/overworld_editor.cc index c0b6b5e4..31bbb01a 100644 --- a/src/app/editor/overworld/overworld_editor.cc +++ b/src/app/editor/overworld/overworld_editor.cc @@ -15,7 +15,6 @@ #include "absl/strings/str_format.h" #include "app/core/asar_wrapper.h" #include "app/core/features.h" -#include "app/core/window.h" #include "app/editor/overworld/map_properties.h" #include "app/editor/overworld/tile16_editor.h" #include "app/gfx/arena.h" @@ -43,7 +42,6 @@ namespace yaze::editor { -using core::Renderer; using namespace ImGui; constexpr float kInputFieldSize = 30.f; @@ -710,7 +708,8 @@ void OverworldEditor::RenderUpdatedMapBitmap( current_bitmap.set_modified(true); // Immediately update the texture to reflect changes - core::Renderer::Get().UpdateBitmap(¤t_bitmap); + // TODO: Queue texture for later rendering. + // core::Renderer::Get().UpdateBitmap(¤t_bitmap); } void OverworldEditor::CheckForOverworldEdits() { @@ -1092,11 +1091,13 @@ absl::Status OverworldEditor::CheckForCurrentMap() { // Ensure tile16 blockset is fully updated before rendering if (tile16_blockset_.atlas.is_active()) { - Renderer::Get().UpdateBitmap(&tile16_blockset_.atlas); + // TODO: Queue texture for later rendering. + // Renderer::Get().UpdateBitmap(&tile16_blockset_.atlas); } // Update map texture with the traditional direct update approach - Renderer::Get().UpdateBitmap(&maps_bmp_[current_map_]); + // TODO: Queue texture for later rendering. + // Renderer::Get().UpdateBitmap(&maps_bmp_[current_map_]); maps_bmp_[current_map_].set_modified(false); } @@ -1411,9 +1412,10 @@ absl::Status OverworldEditor::DrawAreaGraphics() { overworld_.set_current_map(current_map_); palette_ = overworld_.current_area_palette(); gfx::Bitmap bmp; - Renderer::Get().CreateAndRenderBitmap(0x80, kOverworldMapSize, 0x08, - overworld_.current_graphics(), bmp, - palette_); + // TODO: Queue texture for later rendering. + // Renderer::Get().CreateAndRenderBitmap(0x80, kOverworldMapSize, 0x08, + // overworld_.current_graphics(), bmp, + // palette_); current_graphics_set_[current_map_] = bmp; } } @@ -1483,17 +1485,19 @@ absl::Status OverworldEditor::LoadGraphics() { // This avoids blocking the main thread with GPU texture creation { gfx::ScopedTimer gfx_timer("CreateBitmapWithoutTexture_Graphics"); - Renderer::Get().CreateBitmapWithoutTexture(0x80, kOverworldMapSize, 0x40, - overworld_.current_graphics(), - current_gfx_bmp_, palette_); + // TODO: Queue texture for later rendering. + // Renderer::Get().CreateBitmapWithoutTexture(0x80, kOverworldMapSize, 0x40, + // overworld_.current_graphics(), + // current_gfx_bmp_, palette_); } LOG_DEBUG("OverworldEditor", "Loading overworld tileset (deferred textures)."); { gfx::ScopedTimer tileset_timer("CreateBitmapWithoutTexture_Tileset"); - Renderer::Get().CreateBitmapWithoutTexture( - 0x80, 0x2000, 0x08, overworld_.tile16_blockset_data(), - tile16_blockset_bmp_, palette_); + // TODO: Queue texture for later rendering. + // Renderer::Get().CreateBitmapWithoutTexture( + // 0x80, 0x2000, 0x08, overworld_.tile16_blockset_data(), + // tile16_blockset_bmp_, palette_); } map_blockset_loaded_ = true; @@ -1504,8 +1508,14 @@ absl::Status OverworldEditor::LoadGraphics() { { gfx::ScopedTimer tilemap_timer("CreateTilemap"); tile16_blockset_ = - gfx::CreateTilemap(tile16_blockset_data, 0x80, 0x2000, kTile16Size, + gfx::CreateTilemap(renderer_,tile16_blockset_data, 0x80, 0x2000, kTile16Size, zelda3::kNumTile16Individual, palette_); + + // Queue texture creation for the tile16 blockset atlas + if (tile16_blockset_.atlas.is_active() && tile16_blockset_.atlas.surface()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &tile16_blockset_.atlas); + } } // Phase 2: Create bitmaps only for essential maps initially @@ -1566,7 +1576,9 @@ absl::Status OverworldEditor::LoadGraphics() { { gfx::ScopedTimer initial_textures_timer("CreateInitialTextures"); for (int i = 0; i < initial_texture_count; ++i) { - Renderer::Get().RenderBitmap(maps_to_texture[i]); + // Queue texture creation/update for initial maps via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, maps_to_texture[i]); } } @@ -1588,7 +1600,10 @@ absl::Status OverworldEditor::LoadGraphics() { priority = (map_world == current_world_) ? 5 : 15; // Current world = priority 5, others = 15 } - gfx::Arena::Get().QueueDeferredTexture(maps_to_texture[i], priority); + // Queue texture creation for remaining maps via Arena's deferred system + // Note: Priority system to be implemented in future enhancement + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, maps_to_texture[i]); } if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) { @@ -1617,20 +1632,16 @@ absl::Status OverworldEditor::LoadSpriteGraphics() { sprite_previews_[sprite.id()].Create(width, height, depth, *sprite.preview_graphics()); sprite_previews_[sprite.id()].SetPalette(palette_); - Renderer::Get().RenderBitmap(&(sprite_previews_[sprite.id()])); + // TODO: Queue texture for later rendering. + // Renderer::Get().RenderBitmap(&(sprite_previews_[sprite.id()])); } return absl::OkStatus(); } void OverworldEditor::ProcessDeferredTextures() { - // Use Arena's centralized progressive loading system - // This makes progressive loading available to all editors - auto batch = gfx::Arena::Get().GetNextDeferredTextureBatch(4, 2); - - for (auto* bitmap : batch) { - if (bitmap && !bitmap->texture()) { - Renderer::Get().RenderBitmap(bitmap); - } + // Process queued texture commands via Arena's deferred system + if (renderer_) { + gfx::Arena::Get().ProcessTextureQueue(renderer_); } // Also process deferred map refreshes for modified maps @@ -1682,8 +1693,9 @@ void OverworldEditor::EnsureMapTexture(int map_index) { } if (!bitmap.texture() && bitmap.is_active()) { - Renderer::Get().RenderBitmap(&bitmap); - // Note: Arena automatically removes from deferred queue when textures are created + // Queue texture creation for this map + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &bitmap); } } @@ -1783,11 +1795,14 @@ void OverworldEditor::RefreshChildMapOnDemand(int map_index) { map_index); } - // CRITICAL FIX: Force COMPLETE texture recreation for immediate visibility - // UpdateBitmap() was still deferred - we need to force a full re-render - - // Always recreate the texture to ensure immediate GPU update - Renderer::Get().RenderBitmap(&maps_bmp_[map_index]); + // Queue texture update to ensure changes are visible + if (maps_bmp_[map_index].texture()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &maps_bmp_[map_index]); + } else { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &maps_bmp_[map_index]); + } } // Handle multi-area maps (large, wide, tall) with safe coordination @@ -1933,9 +1948,10 @@ void OverworldEditor::RefreshMultiAreaMapsSafely(int map_index, } maps_bmp_[sibling].set_modified(false); - // Update texture if it exists + // Queue texture update/creation if (maps_bmp_[sibling].texture()) { - core::Renderer::Get().UpdateBitmap(&maps_bmp_[sibling]); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &maps_bmp_[sibling]); } else { EnsureMapTexture(sibling); } @@ -2192,8 +2208,19 @@ absl::Status OverworldEditor::RefreshTile16Blockset() { const auto tile16_data = overworld_.tile16_blockset_data(); - gfx::UpdateTilemap(tile16_blockset_, tile16_data); + gfx::UpdateTilemap(renderer_, tile16_blockset_, tile16_data); tile16_blockset_.atlas.SetPalette(palette_); + + // Queue texture update for the atlas + if (tile16_blockset_.atlas.texture() && tile16_blockset_.atlas.is_active()) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_.atlas); + } else if (!tile16_blockset_.atlas.texture() && tile16_blockset_.atlas.is_active()) { + // Create texture if it doesn't exist yet + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &tile16_blockset_.atlas); + } + return absl::OkStatus(); } diff --git a/src/app/editor/overworld/overworld_editor.h b/src/app/editor/overworld/overworld_editor.h index c43663c2..14cf9bf8 100644 --- a/src/app/editor/overworld/overworld_editor.h +++ b/src/app/editor/overworld/overworld_editor.h @@ -68,6 +68,7 @@ class OverworldEditor : public Editor, public gfx::GfxContext { // MapPropertiesSystem will be initialized after maps_bmp_ and canvas are ready } + void Initialize(gfx::IRenderer* renderer, Rom* rom); void Initialize() override; absl::Status Load() override; absl::Status Update() final; @@ -326,6 +327,7 @@ class OverworldEditor : public Editor, public gfx::GfxContext { Rom* rom_; + gfx::IRenderer* renderer_; Tile16Editor tile16_editor_{rom_, &tile16_blockset_}; GfxGroupEditor gfx_group_editor_; PaletteEditor palette_editor_; diff --git a/src/app/editor/overworld/scratch_space.cc b/src/app/editor/overworld/scratch_space.cc index c4dd21bd..dbebe17e 100644 --- a/src/app/editor/overworld/scratch_space.cc +++ b/src/app/editor/overworld/scratch_space.cc @@ -138,7 +138,9 @@ absl::Status OverworldEditor::DrawScratchSpace() { if (all_gfx_loaded_) { palette_ = overworld_.current_area_palette(); current_slot.scratch_bitmap.SetPalette(palette_); - core::Renderer::Get().RenderBitmap(¤t_slot.scratch_bitmap); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, ¤t_slot.scratch_bitmap); } } @@ -315,9 +317,9 @@ void OverworldEditor::UpdateScratchBitmapTile(int tile_x, int tile_y, } scratch_slot.scratch_bitmap.set_modified(true); - // Use batch operations for texture updates - scratch_slot.scratch_bitmap.QueueTextureUpdate( - core::Renderer::Get().renderer()); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &scratch_slot.scratch_bitmap); scratch_slot.in_use = true; } @@ -360,7 +362,9 @@ absl::Status OverworldEditor::SaveCurrentSelectionToScratch(int slot) { if (all_gfx_loaded_) { palette_ = overworld_.current_area_palette(); scratch_spaces_[slot].scratch_bitmap.SetPalette(palette_); - core::Renderer::Get().RenderBitmap( + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &scratch_spaces_[slot].scratch_bitmap); } @@ -398,8 +402,6 @@ absl::Status OverworldEditor::SaveCurrentSelectionToScratch(int slot) { scratch_spaces_[slot].in_use = true; } - // Process all queued texture updates at once - gfx::Arena::Get().ProcessBatchTextureUpdates(); return absl::OkStatus(); } @@ -433,7 +435,9 @@ absl::Status OverworldEditor::ClearScratchSpace(int slot) { auto& data = scratch_spaces_[slot].scratch_bitmap.mutable_data(); std::fill(data.begin(), data.end(), 0); scratch_spaces_[slot].scratch_bitmap.set_modified(true); - core::Renderer::Get().UpdateBitmap(&scratch_spaces_[slot].scratch_bitmap); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &scratch_spaces_[slot].scratch_bitmap); } return absl::OkStatus(); diff --git a/src/app/editor/overworld/tile16_editor.cc b/src/app/editor/overworld/tile16_editor.cc index 9b3142b3..4fca0de0 100644 --- a/src/app/editor/overworld/tile16_editor.cc +++ b/src/app/editor/overworld/tile16_editor.cc @@ -3,13 +3,12 @@ #include #include "absl/status/status.h" -#include "app/core/window.h" #include "app/gfx/arena.h" #include "app/gfx/bitmap.h" +#include "app/gfx/backend/irenderer.h" #include "app/gfx/performance_profiler.h" #include "app/gfx/snes_palette.h" #include "app/gui/canvas.h" -#include "app/gui/icons.h" #include "app/gui/input.h" #include "app/gui/style.h" #include "app/rom.h" @@ -22,7 +21,6 @@ namespace yaze { namespace editor { -using core::Renderer; using namespace ImGui; absl::Status Tile16Editor::Initialize( @@ -34,14 +32,16 @@ absl::Status Tile16Editor::Initialize( current_gfx_bmp_.Create(current_gfx_bmp.width(), current_gfx_bmp.height(), current_gfx_bmp.depth(), current_gfx_bmp.vector()); current_gfx_bmp_.SetPalette(current_gfx_bmp.palette()); // Temporary palette - core::Renderer::Get().RenderBitmap(¤t_gfx_bmp_); + // TODO: Queue texture for later rendering. + // core::Renderer::Get().RenderBitmap(¤t_gfx_bmp_); // Copy the tile16 blockset bitmap tile16_blockset_bmp_.Create( tile16_blockset_bmp.width(), tile16_blockset_bmp.height(), tile16_blockset_bmp.depth(), tile16_blockset_bmp.vector()); tile16_blockset_bmp_.SetPalette(tile16_blockset_bmp.palette()); - core::Renderer::Get().RenderBitmap(&tile16_blockset_bmp_); + // TODO: Queue texture for later rendering. + // core::Renderer::Get().RenderBitmap(&tile16_blockset_bmp_); // Note: LoadTile8() will be called after palette is set by overworld editor // This ensures proper palette coordination from the start @@ -50,7 +50,8 @@ absl::Status Tile16Editor::Initialize( current_tile16_bmp_.Create(kTile16Size, kTile16Size, 8, std::vector(kTile16PixelCount, 0)); current_tile16_bmp_.SetPalette(tile16_blockset_bmp.palette()); - core::Renderer::Get().RenderBitmap(¤t_tile16_bmp_); + // TODO: Queue texture for later rendering. + // core::Renderer::Get().RenderBitmap(¤t_tile16_bmp_); // Initialize enhanced canvas features with proper sizing tile16_edit_canvas_.InitializeDefaults(); @@ -345,8 +346,9 @@ absl::Status Tile16Editor::RefreshTile16Blockset() { // Mark atlas as modified to trigger regeneration tile16_blockset_->atlas.set_modified(true); - // Update the atlas bitmap using the safer direct approach - core::Renderer::Get().UpdateBitmap(&tile16_blockset_->atlas); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas); util::logf("Tile16 blockset refreshed and regenerated"); return absl::OkStatus(); @@ -390,9 +392,10 @@ absl::Status Tile16Editor::UpdateBlocksetBitmap() { } } - // Mark the blockset bitmap as modified and use batch texture update + // Mark the blockset bitmap as modified and queue texture update tile16_blockset_bmp_.set_modified(true); - tile16_blockset_bmp_.QueueTextureUpdate(nullptr); // Use batch operations + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_bmp_); // Also update the tile16 blockset atlas if available if (tile16_blockset_->atlas.is_active()) { @@ -415,11 +418,9 @@ absl::Status Tile16Editor::UpdateBlocksetBitmap() { } tile16_blockset_->atlas.set_modified(true); - tile16_blockset_->atlas.QueueTextureUpdate(nullptr); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas); } - - // Process all queued texture updates at once - gfx::Arena::Get().ProcessBatchTextureUpdates(); } return absl::OkStatus(); @@ -515,8 +516,9 @@ absl::Status Tile16Editor::RegenerateTile16BitmapFromROM() { } } - // Render the updated bitmap - core::Renderer::Get().RenderBitmap(¤t_tile16_bmp_); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, ¤t_tile16_bmp_); util::logf("Regenerated Tile16 bitmap for tile %d from ROM data", current_tile16_); @@ -606,9 +608,10 @@ absl::Status Tile16Editor::DrawToCurrentTile16(ImVec2 pos, } } - // Mark the bitmap as modified and update the renderer + // Mark the bitmap as modified and queue texture update current_tile16_bmp_.set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); // Update ROM data when painting to tile16 auto* tile_data = GetCurrentTile16Data(); @@ -893,8 +896,9 @@ absl::Status Tile16Editor::UpdateTile16Edit() { } } - // Render the display tile - core::Renderer::Get().RenderBitmap(&display_tile); + // Queue texture creation for display tile + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &display_tile); // CRITICAL FIX: Handle tile painting with simple click instead of click+drag // Draw the preview first @@ -1273,7 +1277,9 @@ absl::Status Tile16Editor::LoadTile8() { // Fallback to ROM palette tile_bitmap.SetPalette(rom()->palette_group().overworld_main[0]); } - core::Renderer::Get().RenderBitmap(&tile_bitmap); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &tile_bitmap); } catch (const std::exception& e) { util::logf("Error creating tile at (%d,%d): %s", tile_x, tile_y, e.what()); @@ -1362,8 +1368,9 @@ absl::Status Tile16Editor::SetCurrentTile(int tile_id) { current_tile16_bmp_.SetPalette(rom()->palette_group().overworld_main[0]); } - // Render the bitmap - core::Renderer::Get().RenderBitmap(¤t_tile16_bmp_); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, ¤t_tile16_bmp_); // Simple success logging util::logf("SetCurrentTile: loaded tile %d successfully", tile_id); @@ -1382,7 +1389,9 @@ absl::Status Tile16Editor::CopyTile16ToClipboard(int tile_id) { clipboard_tile16_.Create(16, 16, 8, tile_data); clipboard_tile16_.SetPalette(tile16_blockset_->atlas.palette()); } - core::Renderer::Get().RenderBitmap(&clipboard_tile16_); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &clipboard_tile16_); clipboard_has_data_ = true; return absl::OkStatus(); @@ -1396,7 +1405,9 @@ absl::Status Tile16Editor::PasteTile16FromClipboard() { // Copy the clipboard data to the current tile16 current_tile16_bmp_.Create(16, 16, 8, clipboard_tile16_.vector()); current_tile16_bmp_.SetPalette(clipboard_tile16_.palette()); - core::Renderer::Get().RenderBitmap(¤t_tile16_bmp_); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, ¤t_tile16_bmp_); return absl::OkStatus(); } @@ -1409,7 +1420,9 @@ absl::Status Tile16Editor::SaveTile16ToScratchSpace(int slot) { // Create a copy of the current tile16 bitmap scratch_space_[slot].Create(16, 16, 8, current_tile16_bmp_.vector()); scratch_space_[slot].SetPalette(current_tile16_bmp_.palette()); - core::Renderer::Get().RenderBitmap(&scratch_space_[slot]); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, &scratch_space_[slot]); scratch_space_used_[slot] = true; return absl::OkStatus(); @@ -1427,7 +1440,9 @@ absl::Status Tile16Editor::LoadTile16FromScratchSpace(int slot) { // Copy the scratch space data to the current tile16 current_tile16_bmp_.Create(16, 16, 8, scratch_space_[slot].vector()); current_tile16_bmp_.SetPalette(scratch_space_[slot].palette()); - core::Renderer::Get().RenderBitmap(¤t_tile16_bmp_); + // Queue texture creation via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, ¤t_tile16_bmp_); return absl::OkStatus(); } @@ -1471,7 +1486,9 @@ absl::Status Tile16Editor::FlipTile16Horizontal() { current_tile16_bmp_.SetPalette(palette_); current_tile16_bmp_.set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); return absl::OkStatus(); } @@ -1504,7 +1521,9 @@ absl::Status Tile16Editor::FlipTile16Vertical() { current_tile16_bmp_.SetPalette(palette_); current_tile16_bmp_.set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); return absl::OkStatus(); } @@ -1537,7 +1556,9 @@ absl::Status Tile16Editor::RotateTile16() { current_tile16_bmp_.SetPalette(palette_); current_tile16_bmp_.set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); return absl::OkStatus(); } @@ -1574,7 +1595,9 @@ absl::Status Tile16Editor::FillTile16WithTile8(int tile8_id) { } current_tile16_bmp_.set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); return absl::OkStatus(); } @@ -1590,7 +1613,9 @@ absl::Status Tile16Editor::ClearTile16() { std::fill(data.begin(), data.end(), 0); current_tile16_bmp_.set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); return absl::OkStatus(); } @@ -1633,7 +1658,9 @@ absl::Status Tile16Editor::PreviewPaletteChange(uint8_t palette_id) { const auto& ow_main_pal_group = rom()->palette_group().overworld_main; if (ow_main_pal_group.size() > palette_id) { preview_tile16_.SetPaletteWithTransparent(ow_main_pal_group[0], palette_id); - core::Renderer::Get().UpdateBitmap(&preview_tile16_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &preview_tile16_); preview_dirty_ = true; } @@ -1691,7 +1718,9 @@ absl::Status Tile16Editor::Undo() { y_flip = previous_state.y_flip; priority_tile = previous_state.priority; - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); undo_stack_.pop_back(); return absl::OkStatus(); @@ -1714,7 +1743,9 @@ absl::Status Tile16Editor::Redo() { y_flip = next_state.y_flip; priority_tile = next_state.priority; - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); redo_stack_.pop_back(); return absl::OkStatus(); @@ -1800,7 +1831,9 @@ absl::Status Tile16Editor::UpdateOverworldTilemap() { } tile16_blockset_->atlas.set_modified(true); - core::Renderer::Get().UpdateBitmap(&tile16_blockset_->atlas); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas); } return absl::OkStatus(); @@ -1813,7 +1846,9 @@ absl::Status Tile16Editor::CommitChangesToBlockset() { // Regenerate the tilemap data if needed if (tile16_blockset_->atlas.modified()) { - core::Renderer::Get().UpdateBitmap(&tile16_blockset_->atlas); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas); } // Update individual cached tiles @@ -1857,7 +1892,9 @@ absl::Status Tile16Editor::CommitChangesToOverworld() { } tile16_blockset_->atlas.set_modified(true); - core::Renderer::Get().UpdateBitmap(&tile16_blockset_->atlas); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &tile16_blockset_->atlas); } // Step 4: Notify the parent editor (overworld editor) to regenerate its blockset @@ -2073,7 +2110,9 @@ absl::Status Tile16Editor::UpdateTile8Palette(int tile8_id) { // } current_gfx_individual_[tile8_id].set_modified(true); - Renderer::Get().UpdateBitmap(¤t_gfx_individual_[tile8_id]); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_gfx_individual_[tile8_id]); util::logf("Updated tile8 %d with palette slot %d (palette size: %zu colors)", tile8_id, current_palette_, display_palette.size()); @@ -2125,7 +2164,9 @@ absl::Status Tile16Editor::RefreshAllPalettes() { // Apply the complete 256-color palette to the source bitmap (same as overworld) current_gfx_bmp_.SetPalette(display_palette); current_gfx_bmp_.set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_gfx_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_gfx_bmp_); util::logf( "Applied complete 256-color palette to source bitmap (same as " "overworld)"); @@ -2136,7 +2177,9 @@ absl::Status Tile16Editor::RefreshAllPalettes() { // Use complete 256-color palette (same as overworld system) current_tile16_bmp_.SetPalette(display_palette); current_tile16_bmp_.set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_tile16_bmp_); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_tile16_bmp_); } // Update all individual tile8 graphics with complete 256-color palette @@ -2146,7 +2189,9 @@ absl::Status Tile16Editor::RefreshAllPalettes() { // The pixel data already contains correct color indices for the 256-color palette current_gfx_individual_[i].SetPalette(display_palette); current_gfx_individual_[i].set_modified(true); - core::Renderer::Get().UpdateBitmap(¤t_gfx_individual_[i]); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, ¤t_gfx_individual_[i]); } } diff --git a/src/app/emu/emu.cc b/src/app/emu/emu.cc index 5dfbcfd5..6717801f 100644 --- a/src/app/emu/emu.cc +++ b/src/app/emu/emu.cc @@ -1,5 +1,5 @@ #if __APPLE__ -#include "app/core/platform/app_delegate.h" +#include "app/platform/app_delegate.h" #endif #include @@ -14,6 +14,8 @@ #include "absl/flags/parse.h" #include "app/emu/snes.h" #include "app/rom.h" +#include "app/gfx/backend/irenderer.h" +#include "app/gfx/backend/sdl2_renderer.h" #include "util/sdl_deleter.h" ABSL_FLAG(std::string, emu_rom, "", "Path to the ROM file to load."); @@ -86,17 +88,13 @@ int main(int argc, char **argv) { return EXIT_FAILURE; } - std::unique_ptr renderer_( - SDL_CreateRenderer(window_.get(), -1, - SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC), - SDL_Deleter()); - if (!renderer_) { - printf("SDL_CreateRenderer failed: %s\n", SDL_GetError()); - SDL_Quit(); - return EXIT_FAILURE; + // Create and initialize the renderer + auto renderer = std::make_unique(); + if (!renderer->Initialize(window_.get())) { + printf("Failed to initialize renderer\n"); + SDL_Quit(); + return EXIT_FAILURE; } - SDL_SetRenderDrawBlendMode(renderer_.get(), SDL_BLENDMODE_BLEND); - SDL_SetRenderDrawColor(renderer_.get(), 0x00, 0x00, 0x00, 0xFF); // Initialize audio system constexpr int kAudioFrequency = 48000; @@ -120,10 +118,7 @@ int main(int argc, char **argv) { SDL_PauseAudioDevice(audio_device, 0); // Create PPU texture for rendering - SDL_Texture* ppu_texture = SDL_CreateTexture(renderer_.get(), - SDL_PIXELFORMAT_RGBX8888, - SDL_TEXTUREACCESS_STREAMING, - 512, 480); + void* ppu_texture = renderer->CreateTexture(512, 480); if (!ppu_texture) { printf("SDL_CreateTexture failed: %s\n", SDL_GetError()); SDL_CloseAudioDevice(audio_device); @@ -271,20 +266,17 @@ int main(int argc, char **argv) { // Render PPU output to texture void *ppu_pixels = nullptr; int ppu_pitch = 0; - if (SDL_LockTexture(ppu_texture, nullptr, &ppu_pixels, &ppu_pitch) != 0) { - printf("Failed to lock texture: %s\n", SDL_GetError()); - running = false; - break; + if (renderer->LockTexture(ppu_texture, nullptr, &ppu_pixels, &ppu_pitch)) { + snes_.SetPixels(static_cast(ppu_pixels)); + renderer->UnlockTexture(ppu_texture); } - snes_.SetPixels(static_cast(ppu_pixels)); - SDL_UnlockTexture(ppu_texture); } } // Present rendered frame - SDL_RenderClear(renderer_.get()); - SDL_RenderCopy(renderer_.get(), ppu_texture, nullptr, nullptr); - SDL_RenderPresent(renderer_.get()); + renderer->Clear(); + renderer->RenderCopy(ppu_texture, nullptr, nullptr); + renderer->Present(); } // === Cleanup SDL resources (in reverse order of initialization) === @@ -292,7 +284,7 @@ int main(int argc, char **argv) { // Clean up texture if (ppu_texture) { - SDL_DestroyTexture(ppu_texture); + renderer->DestroyTexture(ppu_texture); ppu_texture = nullptr; } @@ -302,7 +294,7 @@ int main(int argc, char **argv) { SDL_CloseAudioDevice(audio_device); // Clean up renderer and window (done automatically by unique_ptr destructors) - renderer_.reset(); + renderer->Shutdown(); window_.reset(); // Quit SDL subsystems diff --git a/src/app/emu/emu.cmake b/src/app/emu/emu.cmake index 21b533c8..d15dc6ea 100644 --- a/src/app/emu/emu.cmake +++ b/src/app/emu/emu.cmake @@ -5,7 +5,7 @@ if (NOT YAZE_MINIMAL_BUILD AND APPLE) MACOSX_BUNDLE app/main.cc app/rom.cc - app/core/platform/app_delegate.mm + app/platform/app_delegate.mm ${YAZE_APP_EMU_SRC} ${YAZE_APP_CORE_SRC} ${YAZE_APP_EDITOR_SRC} diff --git a/src/app/emu/emulator.cc b/src/app/emu/emulator.cc index 64145f9d..e13c508d 100644 --- a/src/app/emu/emulator.cc +++ b/src/app/emu/emulator.cc @@ -49,21 +49,24 @@ using ImGui::Separator; using ImGui::TableNextColumn; using ImGui::Text; +void Emulator::Initialize(gfx::IRenderer* renderer, const std::vector& rom_data) { + renderer_ = renderer; + rom_data_ = rom_data; + snes_.Init(rom_data_); + initialized_ = true; +} + void Emulator::Run(Rom* rom) { static bool loaded = false; if (!snes_.running() && rom->is_loaded()) { - // Use ARGB8888 format to match PPU output (XBGR layout with format=1) - // Add better error handling and texture optimization - ppu_texture_ = SDL_CreateTexture(core::Renderer::Get().renderer(), - SDL_PIXELFORMAT_ARGB8888, - SDL_TEXTUREACCESS_STREAMING, 512, 480); + ppu_texture_ = renderer_->CreateTexture(512, 480); if (ppu_texture_ == NULL) { printf("Failed to create texture: %s\n", SDL_GetError()); return; } // Optimize texture for better performance - SDL_SetTextureBlendMode(ppu_texture_, SDL_BLENDMODE_NONE); + // renderer_->SetTextureBlendMode(ppu_texture_, SDL_BLENDMODE_NONE); rom_data_ = rom->vector(); snes_.Init(rom_data_); @@ -149,10 +152,9 @@ void Emulator::Run(Rom* rom) { // Update PPU texture only on rendered frames void* ppu_pixels_; int ppu_pitch_; - if (SDL_LockTexture(ppu_texture_, NULL, &ppu_pixels_, &ppu_pitch_) == - 0) { + if (renderer_->LockTexture(ppu_texture_, NULL, &ppu_pixels_, &ppu_pitch_)) { snes_.SetPixels(static_cast(ppu_pixels_)); - SDL_UnlockTexture(ppu_texture_); + renderer_->UnlockTexture(ppu_texture_); } } } diff --git a/src/app/emu/emulator.h b/src/app/emu/emulator.h index bd2a0f0a..d4003aa2 100644 --- a/src/app/emu/emulator.h +++ b/src/app/emu/emulator.h @@ -6,9 +6,11 @@ #include "app/emu/snes.h" #include "app/rom.h" -#include "imgui/imgui.h" namespace yaze { +namespace gfx { +class IRenderer; +} // namespace gfx /** * @namespace yaze::emu @@ -39,6 +41,7 @@ class Emulator { public: Emulator() = default; ~Emulator() = default; + void Initialize(gfx::IRenderer* renderer, const std::vector& rom_data); void Run(Rom* rom); auto snes() -> Snes& { return snes_; } @@ -129,7 +132,9 @@ class Emulator { SDL_AudioDeviceID audio_device_; Snes snes_; - SDL_Texture* ppu_texture_; + bool initialized_ = false; + gfx::IRenderer* renderer_ = nullptr; + void* ppu_texture_ = nullptr; std::vector rom_data_; diff --git a/src/app/gfx/arena.cc b/src/app/gfx/arena.cc index 7d8931d4..0bdbefa9 100644 --- a/src/app/gfx/arena.cc +++ b/src/app/gfx/arena.cc @@ -3,11 +3,14 @@ #include #include +#include "app/gfx/backend/irenderer.h" #include "util/sdl_deleter.h" namespace yaze { namespace gfx { +void Arena::Initialize(IRenderer* renderer) { renderer_ = renderer; } + Arena& Arena::Get() { static Arena instance; return instance; @@ -23,74 +26,95 @@ Arena::~Arena() { Shutdown(); } -/** - * @brief Allocate a new SDL texture with automatic cleanup and resource pooling - * @param renderer SDL renderer for texture creation - * @param width Texture width in pixels - * @param height Texture height in pixels - * @return Pointer to allocated texture (managed by Arena) - * - * Performance Notes: - * - Uses RGBA8888 format for maximum compatibility - * - STREAMING access for dynamic updates (common in ROM editing) - * - Resource pooling for 30% memory reduction - * - Automatic cleanup via unique_ptr with custom deleter - * - Hash map storage for O(1) lookup and management - */ -SDL_Texture* Arena::AllocateTexture(SDL_Renderer* renderer, int width, - int height) { - if (!renderer) { - SDL_Log("Invalid renderer passed to AllocateTexture"); - return nullptr; - } - if (width <= 0 || height <= 0) { - SDL_Log("Invalid texture dimensions: width=%d, height=%d", width, height); - return nullptr; - } - // Try to reuse existing texture of same size from pool - for (auto it = texture_pool_.available_textures_.begin(); - it != texture_pool_.available_textures_.end(); ++it) { - auto& size = texture_pool_.texture_sizes_[*it]; - if (size.first == width && size.second == height) { - SDL_Texture* texture = *it; - texture_pool_.available_textures_.erase(it); - - // Store in hash map with automatic cleanup - textures_[texture] = - std::unique_ptr(texture); - return texture; - } - } - - // Create new texture if none available in pool - return CreateNewTexture(renderer, width, height); +void Arena::QueueTextureCommand(TextureCommandType type, Bitmap* bitmap) { + texture_command_queue_.push_back({type, bitmap}); } -void Arena::FreeTexture(SDL_Texture* texture) { - if (!texture) return; +void Arena::ProcessTextureQueue(IRenderer* renderer) { + if (!renderer_) return; - auto it = textures_.find(texture); - if (it != textures_.end()) { - // Return to pool instead of destroying if pool has space - if (texture_pool_.available_textures_.size() < texture_pool_.MAX_POOL_SIZE) { - // Get texture dimensions before releasing - int width, height; - SDL_QueryTexture(texture, nullptr, nullptr, &width, &height); - texture_pool_.texture_sizes_[texture] = {width, height}; - texture_pool_.available_textures_.push_back(texture); - - // Release from unique_ptr without destroying - it->second.release(); + for (const auto& command : texture_command_queue_) { + switch (command.type) { + case TextureCommandType::CREATE: { + // Create a new texture and update it with bitmap data + if (command.bitmap && command.bitmap->surface() && + command.bitmap->surface()->format && + command.bitmap->is_active() && + command.bitmap->width() > 0 && command.bitmap->height() > 0) { + auto texture = renderer_->CreateTexture(command.bitmap->width(), + command.bitmap->height()); + if (texture) { + command.bitmap->set_texture(texture); + renderer_->UpdateTexture(texture, *command.bitmap); + } + } + break; + } + case TextureCommandType::UPDATE: { + // Update existing texture with current bitmap data + if (command.bitmap && command.bitmap->texture() && + command.bitmap->surface() && command.bitmap->surface()->format && + command.bitmap->is_active()) { + renderer_->UpdateTexture(command.bitmap->texture(), *command.bitmap); + } + break; + } + case TextureCommandType::DESTROY: { + if (command.bitmap && command.bitmap->texture()) { + renderer_->DestroyTexture(command.bitmap->texture()); + command.bitmap->set_texture(nullptr); + } + break; + } } - textures_.erase(it); + } + texture_command_queue_.clear(); +} + +SDL_Surface* Arena::AllocateSurface(int width, int height, int depth, int format) { + // Try to get a surface from the pool first + for (auto it = surface_pool_.available_surfaces_.begin(); + it != surface_pool_.available_surfaces_.end(); ++it) { + auto& info = surface_pool_.surface_info_[*it]; + if (std::get<0>(info) == width && std::get<1>(info) == height && + std::get<2>(info) == depth && std::get<3>(info) == format) { + SDL_Surface* surface = *it; + surface_pool_.available_surfaces_.erase(it); + return surface; + } + } + + // Create new surface if none available in pool + Uint32 sdl_format = GetSnesPixelFormat(format); + SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, depth, sdl_format); + + if (surface) { + auto surface_ptr = std::unique_ptr(surface); + surfaces_[surface] = std::move(surface_ptr); + surface_pool_.surface_info_[surface] = std::make_tuple(width, height, depth, format); + } + + return surface; +} + +void Arena::FreeSurface(SDL_Surface* surface) { + if (!surface) return; + + // Return surface to pool if space available + if (surface_pool_.available_surfaces_.size() < surface_pool_.MAX_POOL_SIZE) { + surface_pool_.available_surfaces_.push_back(surface); + } else { + // Remove from tracking maps + surface_pool_.surface_info_.erase(surface); + surfaces_.erase(surface); } } void Arena::Shutdown() { // Process any remaining batch updates before shutdown - ProcessBatchTextureUpdates(); + ProcessTextureQueue(renderer_); // Clear pool references first to prevent reuse during shutdown surface_pool_.available_surfaces_.clear(); @@ -104,406 +128,9 @@ void Arena::Shutdown() { surfaces_.clear(); // Clear any remaining queue items - batch_update_queue_.clear(); + texture_command_queue_.clear(); } -/** - * @brief Update texture data from surface (with format conversion) - * @param texture Target texture to update - * @param surface Source surface with pixel data - * - * Performance Notes: - * - Converts surface to RGBA8888 format for texture compatibility - * - Uses memcpy for efficient pixel data transfer - * - Handles format conversion automatically - * - Locks texture for direct pixel access - * - * ROM Hacking Specific: - * - Supports indexed color surfaces (common in SNES graphics) - * - Handles palette-based graphics conversion - * - Optimized for frequent updates during editing - */ -void Arena::UpdateTexture(SDL_Texture* texture, SDL_Surface* surface) { - if (!texture || !surface) { - SDL_Log("Invalid texture or surface passed to UpdateTexture"); - return; - } - - if (surface->pixels == nullptr) { - SDL_Log("Surface pixels are nullptr"); - return; - } - - // Additional safety checks to prevent crashes - if (surface->w <= 0 || surface->h <= 0) { - SDL_Log("Invalid surface dimensions: %dx%d", surface->w, surface->h); - return; - } - - if (!surface->format) { - SDL_Log("Surface format is nullptr"); - return; - } - - // Convert surface to RGBA8888 format for texture compatibility - auto converted_surface = - std::unique_ptr( - SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA8888, 0), - util::SDL_Surface_Deleter()); - - if (!converted_surface) { - SDL_Log("SDL_ConvertSurfaceFormat failed: %s", SDL_GetError()); - return; - } - - // Additional validation for converted surface - if (!converted_surface->pixels) { - SDL_Log("Converted surface pixels are nullptr"); - return; - } - - if (converted_surface->w <= 0 || converted_surface->h <= 0) { - SDL_Log("Invalid converted surface dimensions: %dx%d", converted_surface->w, converted_surface->h); - return; - } - - // Validate texture before locking - int texture_w, texture_h; - if (SDL_QueryTexture(texture, nullptr, nullptr, &texture_w, &texture_h) != 0) { - SDL_Log("SDL_QueryTexture failed: %s", SDL_GetError()); - return; - } - - if (texture_w != converted_surface->w || texture_h != converted_surface->h) { - SDL_Log("Texture/surface size mismatch: texture=%dx%d, surface=%dx%d", - texture_w, texture_h, converted_surface->w, converted_surface->h); - return; - } - - // Lock texture for direct pixel access - void* pixels; - int pitch; - if (SDL_LockTexture(texture, nullptr, &pixels, &pitch) != 0) { - SDL_Log("SDL_LockTexture failed: %s", SDL_GetError()); - return; - } - - // Additional safety check for locked pixels - if (!pixels) { - SDL_Log("Locked texture pixels are nullptr"); - SDL_UnlockTexture(texture); - return; - } - - // Validate copy size to prevent buffer overrun - size_t copy_size = converted_surface->h * converted_surface->pitch; - size_t max_texture_size = texture_h * pitch; - - if (copy_size > max_texture_size) { - SDL_Log("Copy size (%zu) exceeds texture capacity (%zu)", copy_size, max_texture_size); - SDL_UnlockTexture(texture); - return; - } - - // Copy pixel data efficiently with bounds checking - memcpy(pixels, converted_surface->pixels, copy_size); - - SDL_UnlockTexture(texture); -} - -SDL_Surface* Arena::AllocateSurface(int width, int height, int depth, - int format) { - // Try to reuse existing surface of same size and format from pool - for (auto it = surface_pool_.available_surfaces_.begin(); - it != surface_pool_.available_surfaces_.end(); ++it) { - auto& info = surface_pool_.surface_info_[*it]; - if (std::get<0>(info) == width && std::get<1>(info) == height && - std::get<2>(info) == depth && std::get<3>(info) == format) { - SDL_Surface* surface = *it; - surface_pool_.available_surfaces_.erase(it); - - // Clear the surface pixels before reusing for safety - if (surface && surface->pixels) { - memset(surface->pixels, 0, surface->h * surface->pitch); - } - - // Store in hash map with automatic cleanup - surfaces_[surface] = - std::unique_ptr(surface); - return surface; - } - } - - // Create new surface if none available in pool - return CreateNewSurface(width, height, depth, format); -} - - -void Arena::FreeSurface(SDL_Surface* surface) { - if (!surface) return; - - auto it = surfaces_.find(surface); - if (it != surfaces_.end()) { - // Return to pool instead of destroying if pool has space - if (surface_pool_.available_surfaces_.size() < surface_pool_.MAX_POOL_SIZE) { - // Get surface info before releasing - int width = surface->w; - int height = surface->h; - int depth = surface->format->BitsPerPixel; - int format = surface->format->format; - surface_pool_.surface_info_[surface] = {width, height, depth, format}; - surface_pool_.available_surfaces_.push_back(surface); - - // Release from unique_ptr without destroying - it->second.release(); - } - surfaces_.erase(it); - } -} - -/** - * @brief Create a new SDL texture (helper for resource pooling) - * @param renderer SDL renderer for texture creation - * @param width Texture width in pixels - * @param height Texture height in pixels - * @return Pointer to allocated texture (managed by Arena) - */ -SDL_Texture* Arena::CreateNewTexture(SDL_Renderer* renderer, int width, int height) { - SDL_Texture* texture = - SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, - SDL_TEXTUREACCESS_STREAMING, width, height); - if (!texture) { - SDL_Log("Failed to create texture: %s", SDL_GetError()); - return nullptr; - } - - // Store in hash map with automatic cleanup - textures_[texture] = - std::unique_ptr(texture); - return texture; -} - -/** - * @brief Create a new SDL surface (helper for resource pooling) - * @param width Surface width in pixels - * @param height Surface height in pixels - * @param depth Color depth in bits per pixel - * @param format SDL pixel format - * @return Pointer to allocated surface (managed by Arena) - */ -SDL_Surface* Arena::CreateNewSurface(int width, int height, int depth, int format) { - SDL_Surface* surface = - SDL_CreateRGBSurfaceWithFormat(0, width, height, depth, format); - if (!surface) { - SDL_Log("Failed to create surface: %s", SDL_GetError()); - return nullptr; - } - - // Store in hash map with automatic cleanup - surfaces_[surface] = - std::unique_ptr(surface); - return surface; -} - -/** - * @brief Update texture data from surface for a specific region - * @param texture Target texture to update - * @param surface Source surface with pixel data - * @param rect Region to update (nullptr for entire texture) - * - * Performance Notes: - * - Region-specific updates for efficiency - * - Converts surface to RGBA8888 format for texture compatibility - * - Uses memcpy for efficient pixel data transfer - * - Handles format conversion automatically - */ -void Arena::UpdateTextureRegion(SDL_Texture* texture, SDL_Surface* surface, SDL_Rect* rect) { - if (!texture || !surface) { - return; - } - - if (surface->pixels == nullptr) { - return; - } - - // Convert surface to RGBA8888 format for texture compatibility - auto converted_surface = - std::unique_ptr( - SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA8888, 0), - util::SDL_Surface_Deleter()); - - if (!converted_surface) { - return; - } - - // Lock texture for direct pixel access - void* pixels; - int pitch; - if (SDL_LockTexture(texture, rect, &pixels, &pitch) != 0) { - return; - } - - // Copy pixel data efficiently with bounds checking - if (rect) { - // Validate rect bounds against surface dimensions - int max_x = std::min(rect->x + rect->w, converted_surface->w); - int max_y = std::min(rect->y + rect->h, converted_surface->h); - int safe_x = std::max(0, rect->x); - int safe_y = std::max(0, rect->y); - int safe_w = max_x - safe_x; - int safe_h = max_y - safe_y; - - - if (safe_w > 0 && safe_h > 0) { - // Copy only the safe region - int src_offset = safe_y * converted_surface->pitch + safe_x * 4; // 4 bytes per RGBA pixel - int dst_offset = 0; - for (int y = 0; y < safe_h; y++) { - // Additional safety check for each row - if (src_offset + safe_w * 4 <= converted_surface->h * converted_surface->pitch) { - memcpy(static_cast(pixels) + dst_offset, - static_cast(converted_surface->pixels) + src_offset, - safe_w * 4); - } - src_offset += converted_surface->pitch; - dst_offset += pitch; - } - } - } else { - // Copy entire surface - memcpy(pixels, converted_surface->pixels, - converted_surface->h * converted_surface->pitch); - } - - SDL_UnlockTexture(texture); -} - -/** - * @brief Queue a texture update for batch processing - * @param texture Target texture to update - * @param surface Source surface with pixel data - * @param rect Region to update (nullptr for entire texture) - * - * Performance Notes: - * - Queues updates instead of processing immediately - * - Reduces SDL calls by batching multiple updates - * - Automatic queue size management to prevent memory bloat - */ -void Arena::QueueTextureUpdate(SDL_Texture* texture, SDL_Surface* surface, SDL_Rect* rect) { - if (!texture || !surface) { - SDL_Log("Invalid texture or surface passed to QueueTextureUpdate"); - return; - } - - // Prevent queue from growing too large - if (batch_update_queue_.size() >= MAX_BATCH_SIZE) { - ProcessBatchTextureUpdates(); - } - - batch_update_queue_.emplace_back(texture, surface, rect); -} - -/** - * @brief Process all queued texture updates in a single batch - * @note This reduces SDL calls and improves performance significantly - * - * Performance Notes: - * - Processes all queued updates in one operation - * - Reduces SDL context switching overhead - * - Optimized for multiple small updates - * - Clears queue after processing - */ -void Arena::ProcessBatchTextureUpdates() { - if (batch_update_queue_.empty()) { - return; - } - - // Process all queued updates with minimal logging - for (const auto& update : batch_update_queue_) { - // Validate pointers before processing - if (!update.texture || !update.surface) { - continue; - } - - if (update.rect) { - UpdateTextureRegion(update.texture, update.surface, update.rect.get()); - } else { - UpdateTexture(update.texture, update.surface); - } - } - - // Clear the queue after processing - batch_update_queue_.clear(); -} - -/** - * @brief Clear all queued texture updates - * @note Useful for cleanup or when batch processing is not needed - */ -void Arena::ClearBatchQueue() { - batch_update_queue_.clear(); -} - -// ============================================================================ -// Progressive/Deferred Texture Management -// ============================================================================ - -void Arena::QueueDeferredTexture(gfx::Bitmap* bitmap, int priority) { - if (!bitmap) return; - - std::lock_guard lock(deferred_mutex_); - deferred_textures_.emplace_back(bitmap, priority); -} - -std::vector Arena::GetNextDeferredTextureBatch( - int high_priority_limit, int low_priority_limit) { - std::lock_guard lock(deferred_mutex_); - - std::vector batch; - - if (deferred_textures_.empty()) { - return batch; - } - - // Sort by priority (lower number = higher priority) - std::sort(deferred_textures_.begin(), deferred_textures_.end(), - [](const DeferredTexture& a, const DeferredTexture& b) { - return a.priority < b.priority; - }); - - // Phase 1: Collect high-priority items (priority 0-10) - auto it = deferred_textures_.begin(); - while (it != deferred_textures_.end() && batch.size() < static_cast(high_priority_limit)) { - if (it->bitmap && it->priority <= 10 && !it->bitmap->texture()) { - batch.push_back(it->bitmap); - it = deferred_textures_.erase(it); - } else { - ++it; - } - } - - // Phase 2: Collect low-priority items (priority 11+) if we have capacity - if (batch.size() < static_cast(high_priority_limit)) { - it = deferred_textures_.begin(); - int low_count = 0; - while (it != deferred_textures_.end() && low_count < low_priority_limit) { - if (it->bitmap && it->priority > 10 && !it->bitmap->texture()) { - batch.push_back(it->bitmap); - low_count++; - it = deferred_textures_.erase(it); - } else { - ++it; - } - } - } - - return batch; -} - -void Arena::ClearDeferredTextures() { - std::lock_guard lock(deferred_mutex_); - deferred_textures_.clear(); -} } // namespace gfx } // namespace yaze \ No newline at end of file diff --git a/src/app/gfx/arena.h b/src/app/gfx/arena.h index 373db503..bdc2901a 100644 --- a/src/app/gfx/arena.h +++ b/src/app/gfx/arena.h @@ -45,76 +45,23 @@ class Arena { public: static Arena& Get(); + void Initialize(IRenderer* renderer); ~Arena(); - // Resource management - /** - * @brief Allocate a new SDL texture with automatic cleanup - * @param renderer SDL renderer for texture creation - * @param width Texture width in pixels - * @param height Texture height in pixels - * @return Pointer to allocated texture (managed by Arena) - */ - SDL_Texture* AllocateTexture(SDL_Renderer* renderer, int width, int height); - - /** - * @brief Free a texture and remove from Arena management - * @param texture Texture to free - */ - void FreeTexture(SDL_Texture* texture); - - /** - * @brief Update texture data from surface (with format conversion) - * @param texture Target texture to update - * @param surface Source surface with pixel data - */ - void UpdateTexture(SDL_Texture* texture, SDL_Surface* surface); + // --- New Deferred Command System --- + enum class TextureCommandType { CREATE, UPDATE, DESTROY }; + struct TextureCommand { + TextureCommandType type; + Bitmap* bitmap; // The bitmap that needs a texture operation + }; - /** - * @brief Update texture data from surface for a specific region - * @param texture Target texture to update - * @param surface Source surface with pixel data - * @param rect Region to update (nullptr for entire texture) - */ - void UpdateTextureRegion(SDL_Texture* texture, SDL_Surface* surface, SDL_Rect* rect = nullptr); + void QueueTextureCommand(TextureCommandType type, Bitmap* bitmap); + void ProcessTextureQueue(IRenderer* renderer); - // Batch operations for improved performance - /** - * @brief Queue a texture update for batch processing - * @param texture Target texture to update - * @param surface Source surface with pixel data - * @param rect Region to update (nullptr for entire texture) - */ - void QueueTextureUpdate(SDL_Texture* texture, SDL_Surface* surface, SDL_Rect* rect = nullptr); - - /** - * @brief Process all queued texture updates in a single batch - * @note This reduces SDL calls and improves performance significantly - */ - void ProcessBatchTextureUpdates(); - - /** - * @brief Clear all queued texture updates - */ - void ClearBatchQueue(); - - /** - * @brief Allocate a new SDL surface with automatic cleanup - * @param width Surface width in pixels - * @param height Surface height in pixels - * @param depth Color depth in bits per pixel - * @param format SDL pixel format - * @return Pointer to allocated surface (managed by Arena) - */ + // --- Surface Management (unchanged) --- SDL_Surface* AllocateSurface(int width, int height, int depth, int format); - - /** - * @brief Free a surface and remove from Arena management - * @param surface Surface to free - */ void FreeSurface(SDL_Surface* surface); - // Explicit cleanup method for controlled shutdown void Shutdown(); // Resource tracking for debugging @@ -163,41 +110,6 @@ class Arena { */ auto& bg2() { return bg2_; } - // Progressive/Deferred Texture Management (for large asset loading) - /** - * @brief Add a bitmap to the deferred texture queue - * @param bitmap Bitmap that needs a texture created - * @param priority Higher priority items processed first (0 = highest) - * - * Use this for progressive loading of large asset sets (e.g., overworld maps). - * Textures are created incrementally per frame to avoid UI freezes. - */ - void QueueDeferredTexture(gfx::Bitmap* bitmap, int priority = 0); - - /** - * @brief Get next batch of deferred textures to process - * @param high_priority_limit Max high-priority items to return - * @param low_priority_limit Max low-priority items to return - * @return Vector of bitmaps to render (caller renders them via Renderer) - * - * Call this once per frame in your editor's Update() method, then render each bitmap. - * High-priority items (priority 0-10) returned up to high_priority_limit. - * Low-priority items (priority 11+) returned up to low_priority_limit. - */ - std::vector GetNextDeferredTextureBatch(int high_priority_limit = 4, - int low_priority_limit = 2); - - /** - * @brief Clear all deferred texture items - */ - void ClearDeferredTextures(); - - /** - * @brief Get count of remaining deferred textures - * @return Number of bitmaps waiting for textures - */ - size_t GetDeferredTextureCount() const { return deferred_textures_.size(); } - private: Arena(); @@ -213,7 +125,7 @@ class Arena { std::array gfx_sheets_; - std::unordered_map> textures_; @@ -223,8 +135,8 @@ class Arena { // Resource pooling for efficient memory management struct TexturePool { - std::vector available_textures_; - std::unordered_map> texture_sizes_; + std::vector available_textures_; + std::unordered_map> texture_sizes_; static constexpr size_t MAX_POOL_SIZE = 100; } texture_pool_; @@ -234,32 +146,8 @@ class Arena { static constexpr size_t MAX_POOL_SIZE = 100; } surface_pool_; - // Batch operations for improved performance - struct BatchUpdate { - SDL_Texture* texture; - SDL_Surface* surface; - std::unique_ptr rect; - - BatchUpdate(SDL_Texture* t, SDL_Surface* s, SDL_Rect* r = nullptr) - : texture(t), surface(s), rect(r ? std::make_unique(*r) : nullptr) {} - }; - - std::vector batch_update_queue_; - static constexpr size_t MAX_BATCH_SIZE = 50; - - // Helper methods for resource pooling - SDL_Texture* CreateNewTexture(SDL_Renderer* renderer, int width, int height); - SDL_Surface* CreateNewSurface(int width, int height, int depth, int format); - - // Progressive loading infrastructure - struct DeferredTexture { - gfx::Bitmap* bitmap; - int priority; - - DeferredTexture(gfx::Bitmap* bmp, int prio) : bitmap(bmp), priority(prio) {} - }; - std::vector deferred_textures_; - std::mutex deferred_mutex_; + std::vector texture_command_queue_; + IRenderer* renderer_ = nullptr; }; } // namespace gfx diff --git a/src/app/gfx/atlas_renderer.cc b/src/app/gfx/atlas_renderer.cc index 07cdd5a1..aa68fbb0 100644 --- a/src/app/gfx/atlas_renderer.cc +++ b/src/app/gfx/atlas_renderer.cc @@ -12,7 +12,7 @@ AtlasRenderer& AtlasRenderer::Get() { return instance; } -void AtlasRenderer::Initialize(SDL_Renderer* renderer, int initial_size) { +void AtlasRenderer::Initialize(IRenderer* renderer, int initial_size) { renderer_ = renderer; next_atlas_id_ = 0; current_atlas_ = 0; @@ -37,15 +37,10 @@ int AtlasRenderer::AddBitmap(const Bitmap& bitmap) { int atlas_id = next_atlas_id_++; auto& atlas = *atlases_[current_atlas_]; - // Create atlas entry with BPP format information - BppFormat bpp_format = BppFormatManager::Get().DetectFormat(bitmap.vector(), bitmap.width(), bitmap.height()); - atlas.entries.emplace_back(atlas_id, uv_rect, bitmap.texture(), bpp_format, bitmap.width(), bitmap.height()); - atlas_lookup_[atlas_id] = &atlas.entries.back(); - // Copy bitmap data to atlas texture - SDL_SetRenderTarget(renderer_, atlas.texture); - SDL_RenderCopy(renderer_, bitmap.texture(), nullptr, &uv_rect); - SDL_SetRenderTarget(renderer_, nullptr); + renderer_->SetRenderTarget(atlas.texture); + renderer_->RenderCopy(bitmap.texture(), nullptr, &uv_rect); + renderer_->SetRenderTarget(nullptr); return atlas_id; } @@ -61,9 +56,9 @@ int AtlasRenderer::AddBitmap(const Bitmap& bitmap) { atlas_lookup_[atlas_id] = &atlas.entries.back(); // Copy bitmap data to atlas texture - SDL_SetRenderTarget(renderer_, atlas.texture); - SDL_RenderCopy(renderer_, bitmap.texture(), nullptr, &uv_rect); - SDL_SetRenderTarget(renderer_, nullptr); + renderer_->SetRenderTarget(atlas.texture); + renderer_->RenderCopy(bitmap.texture(), nullptr, &uv_rect); + renderer_->SetRenderTarget(nullptr); return atlas_id; } @@ -92,7 +87,7 @@ int AtlasRenderer::AddBitmapWithBppOptimization(const Bitmap& bitmap, BppFormat // Create temporary bitmap with converted data Bitmap converted_bitmap(bitmap.width(), bitmap.height(), bitmap.depth(), converted_data, bitmap.palette()); - converted_bitmap.CreateTexture(renderer_); + converted_bitmap.CreateTexture(); // Add converted bitmap to atlas return AddBitmap(converted_bitmap); @@ -169,7 +164,7 @@ void AtlasRenderer::RenderBatch(const std::vector& render_command auto& atlas = *atlases_[atlas_index]; // Set atlas texture - SDL_SetTextureBlendMode(atlas.texture, SDL_BLENDMODE_BLEND); + // SDL_SetTextureBlendMode(atlas.texture, SDL_BLENDMODE_BLEND); // Render all commands for this atlas for (const auto* cmd : commands) { @@ -190,9 +185,9 @@ void AtlasRenderer::RenderBatch(const std::vector& render_command if (std::abs(cmd->rotation) > 0.001F) { // For rotation, we'd need to use SDL_RenderCopyEx // This is a simplified version - SDL_RenderCopy(renderer_, atlas.texture, &entry->uv_rect, &dest_rect); + renderer_->RenderCopy(atlas.texture, &entry->uv_rect, &dest_rect); } else { - SDL_RenderCopy(renderer_, atlas.texture, &entry->uv_rect, &dest_rect); + renderer_->RenderCopy(atlas.texture, &entry->uv_rect, &dest_rect); } } } @@ -238,7 +233,7 @@ void AtlasRenderer::RenderBatchWithBppOptimization(const std::vectorrotation) > 0.001F) { - SDL_RenderCopy(renderer_, atlas.texture, &entry->uv_rect, &dest_rect); + renderer_->RenderCopy(atlas.texture, &entry->uv_rect, &dest_rect); } else { - SDL_RenderCopy(renderer_, atlas.texture, &entry->uv_rect, &dest_rect); + renderer_->RenderCopy(atlas.texture, &entry->uv_rect, &dest_rect); } } } @@ -306,7 +301,7 @@ void AtlasRenderer::Clear() { // Clean up SDL textures for (auto& atlas : atlases_) { if (atlas->texture) { - SDL_DestroyTexture(atlas->texture); + renderer_->DestroyTexture(atlas->texture); } } @@ -341,8 +336,8 @@ void AtlasRenderer::RenderBitmap(int atlas_id, float x, float y, float scale_x, }; // Render using atlas texture - SDL_SetTextureBlendMode(atlas->texture, SDL_BLENDMODE_BLEND); - SDL_RenderCopy(renderer_, atlas->texture, &entry->uv_rect, &dest_rect); + // SDL_SetTextureBlendMode(atlas->texture, SDL_BLENDMODE_BLEND); + renderer_->RenderCopy(atlas->texture, &entry->uv_rect, &dest_rect); return; } } @@ -393,8 +388,7 @@ void AtlasRenderer::CreateNewAtlas() { // Create SDL texture for the atlas auto& atlas = *atlases_[current_atlas_]; - atlas.texture = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, - SDL_TEXTUREACCESS_TARGET, size, size); + atlas.texture = renderer_->CreateTexture(size, size); if (!atlas.texture) { SDL_Log("Failed to create atlas texture: %s", SDL_GetError()); @@ -406,18 +400,18 @@ void AtlasRenderer::RebuildAtlas(Atlas& atlas) { std::fill(atlas.used_regions.begin(), atlas.used_regions.end(), false); // Rebuild atlas texture by copying from source textures - SDL_SetRenderTarget(renderer_, atlas.texture); - SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); - SDL_RenderClear(renderer_); + renderer_->SetRenderTarget(atlas.texture); + renderer_->SetDrawColor({0, 0, 0, 0}); + renderer_->Clear(); for (auto& entry : atlas.entries) { if (entry.in_use && entry.texture) { - SDL_RenderCopy(renderer_, entry.texture, nullptr, &entry.uv_rect); + renderer_->RenderCopy(entry.texture, nullptr, &entry.uv_rect); MarkRegionUsed(atlas, entry.uv_rect, true); } } - SDL_SetRenderTarget(renderer_, nullptr); + renderer_->SetRenderTarget(nullptr); } SDL_Rect AtlasRenderer::FindFreeRegion(Atlas& atlas, int width, int height) { diff --git a/src/app/gfx/atlas_renderer.h b/src/app/gfx/atlas_renderer.h index c7692ca4..5966b904 100644 --- a/src/app/gfx/atlas_renderer.h +++ b/src/app/gfx/atlas_renderer.h @@ -77,10 +77,10 @@ class AtlasRenderer { /** * @brief Initialize the atlas renderer - * @param renderer SDL renderer for texture operations + * @param renderer The renderer to use for texture operations * @param initial_size Initial atlas size (power of 2 recommended) */ - void Initialize(SDL_Renderer* renderer, int initial_size = 1024); + void Initialize(IRenderer* renderer, int initial_size = 1024); /** * @brief Add a bitmap to the atlas @@ -164,20 +164,20 @@ class AtlasRenderer { struct AtlasEntry { int atlas_id; SDL_Rect uv_rect; // UV coordinates in atlas - SDL_Texture* texture; + TextureHandle texture; bool in_use; BppFormat bpp_format; // BPP format of this entry int original_width; int original_height; - AtlasEntry(int id, const SDL_Rect& rect, SDL_Texture* tex, BppFormat bpp = BppFormat::kBpp8, + AtlasEntry(int id, const SDL_Rect& rect, TextureHandle tex, BppFormat bpp = BppFormat::kBpp8, int width = 0, int height = 0) : atlas_id(id), uv_rect(rect), texture(tex), in_use(true), bpp_format(bpp), original_width(width), original_height(height) {} }; struct Atlas { - SDL_Texture* texture; + TextureHandle texture; int size; std::vector entries; std::vector used_regions; // Track used regions for packing @@ -185,7 +185,7 @@ class AtlasRenderer { Atlas(int s) : size(s), used_regions(s * s, false) {} }; - SDL_Renderer* renderer_; + IRenderer* renderer_; std::vector> atlases_; std::unordered_map atlas_lookup_; int next_atlas_id_; diff --git a/src/app/gfx/backend/sdl2_renderer.cc b/src/app/gfx/backend/sdl2_renderer.cc index 0fd450f9..9a38433c 100644 --- a/src/app/gfx/backend/sdl2_renderer.cc +++ b/src/app/gfx/backend/sdl2_renderer.cc @@ -56,13 +56,24 @@ TextureHandle SDL2Renderer::CreateTexture(int width, int height) { */ void SDL2Renderer::UpdateTexture(TextureHandle texture, const Bitmap& bitmap) { SDL_Surface* surface = bitmap.surface(); - if (!texture || !surface) return; + + // Validate texture, surface, and surface format + if (!texture || !surface || !surface->format) { + return; + } + + // Validate surface has pixels + if (!surface->pixels || surface->w <= 0 || surface->h <= 0) { + return; + } // Convert the bitmap's surface to RGBA8888 format for compatibility with the texture. auto converted_surface = std::unique_ptr( SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA8888, 0)); - if (!converted_surface) return; + if (!converted_surface || !converted_surface->pixels) { + return; + } // Update the texture with the pixels from the converted surface. SDL_UpdateTexture(static_cast(texture), nullptr, converted_surface->pixels, converted_surface->pitch); diff --git a/src/app/gfx/bitmap.cc b/src/app/gfx/bitmap.cc index 8ac35a08..c5f50d21 100644 --- a/src/app/gfx/bitmap.cc +++ b/src/app/gfx/bitmap.cc @@ -218,6 +218,11 @@ void Bitmap::Create(int width, int height, int depth, int format, SDL_UnlockSurface(surface_); } active_ = true; + + // Apply the stored palette if one exists + if (!palette_.empty()) { + ApplyStoredPalette(); + } } void Bitmap::Reformat(int format) { @@ -234,124 +239,38 @@ void Bitmap::Reformat(int format) { SetPalette(palette_); } -void Bitmap::UpdateTexture(SDL_Renderer *renderer) { - if (!texture_) { - CreateTexture(renderer); - return; - } - - // Use direct SDL calls for reliable texture updates - if (modified_ && surface_ && surface_->pixels) { - // Convert surface to RGBA8888 format for texture compatibility - SDL_Surface* converted = SDL_ConvertSurfaceFormat(surface_, SDL_PIXELFORMAT_RGBA8888, 0); - if (converted) { - // Update texture directly with SDL - int result = SDL_UpdateTexture(texture_, nullptr, converted->pixels, converted->pitch); - if (result != 0) { - SDL_Log("SDL_UpdateTexture failed: %s", SDL_GetError()); - } - SDL_FreeSurface(converted); - } else { - SDL_Log("SDL_ConvertSurfaceFormat failed: %s", SDL_GetError()); - } - modified_ = false; - } +void Bitmap::CreateTexture() { + Arena::Get().QueueTextureCommand(Arena::TextureCommandType::CREATE, this); } -/** - * @brief Queue texture update for batch processing (improved performance) - * @param renderer SDL renderer for texture operations - * @note Use this for better performance when multiple textures need updating - * - * Performance Notes: - * - Queues updates instead of processing immediately - * - Reduces SDL calls by batching multiple updates - * - 5x faster for multiple texture updates - * - Automatic dirty region handling - */ -void Bitmap::QueueTextureUpdate(SDL_Renderer *renderer) { - ScopedTimer timer("texture_batch_queue"); - - if (!texture_) { - CreateTexture(renderer); - return; - } - - // Only queue if there are dirty regions - if (!dirty_region_.is_dirty) { - return; - } - - // Queue the dirty region update for batch processing - if (dirty_region_.is_dirty) { - // Ensure dirty rect is within bounds - int rect_x = std::max(0, dirty_region_.min_x); - int rect_y = std::max(0, dirty_region_.min_y); - int rect_w = std::min(width_ - rect_x, dirty_region_.max_x - dirty_region_.min_x + 1); - int rect_h = std::min(height_ - rect_y, dirty_region_.max_y - dirty_region_.min_y + 1); - - // Only proceed if we have a valid rect - if (rect_w > 0 && rect_h > 0) { - SDL_Rect dirty_rect = { rect_x, rect_y, rect_w, rect_h }; - Arena::Get().QueueTextureUpdate(texture_, surface_, &dirty_rect); - } - dirty_region_.Reset(); - } +void Bitmap::UpdateTexture() { + Arena::Get().QueueTextureCommand(Arena::TextureCommandType::UPDATE, this); } -void Bitmap::CreateTexture(SDL_Renderer *renderer) { - if (!renderer) { - SDL_Log("Invalid renderer passed to CreateTexture"); - return; - } - if (width_ <= 0 || height_ <= 0) { - SDL_Log("Invalid texture dimensions: width=%d, height=%d\n", width_, - height_); - return; - } - // Get a texture from the Arena - texture_ = Arena::Get().AllocateTexture(renderer, width_, height_); - if (!texture_) { - SDL_Log("Bitmap::CreateTexture failed to allocate texture: %s\n", - SDL_GetError()); - return; - } - - UpdateTextureData(); -} - -void Bitmap::UpdateTextureData() { - if (!texture_ || !surface_) { - return; - } - - Arena::Get().UpdateTexture(texture_, surface_); - modified_ = false; -} - -void Bitmap::SetPalette(const SnesPalette &palette) { +void Bitmap::ApplyStoredPalette() { if (surface_ == nullptr) { - throw BitmapError("Surface is null. Palette not applied"); + return; // Can't apply without surface } if (surface_->format == nullptr || surface_->format->palette == nullptr) { - throw BitmapError( - "Surface format or palette is null. Palette not applied."); + return; // Can't apply palette to this surface format + } + if (palette_.empty()) { + return; // No palette to apply } - palette_ = palette; // Invalidate palette cache when palette changes InvalidatePaletteCache(); SDL_Palette *sdl_palette = surface_->format->palette; if (sdl_palette == nullptr) { - throw BitmapError("Failed to get SDL palette"); + return; } SDL_UnlockSurface(surface_); - for (size_t i = 0; i < palette.size(); ++i) { - const auto& pal_color = palette[i]; + for (size_t i = 0; i < palette_.size() && i < 256; ++i) { + const auto& pal_color = palette_[i]; sdl_palette->colors[i].r = pal_color.rgb().x; sdl_palette->colors[i].g = pal_color.rgb().y; sdl_palette->colors[i].b = pal_color.rgb().z; @@ -360,10 +279,22 @@ void Bitmap::SetPalette(const SnesPalette &palette) { SDL_LockSurface(surface_); } +void Bitmap::SetPalette(const SnesPalette &palette) { + // Store palette even if surface isn't ready yet + palette_ = palette; + + // Apply it immediately if surface is ready + ApplyStoredPalette(); +} + void Bitmap::SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length) { + // Store palette even if surface isn't ready yet + palette_ = palette; + + // If surface isn't created yet, just store the palette for later if (surface_ == nullptr) { - throw BitmapError("Surface is null. Palette not applied"); + return; // Palette will be applied when surface is created } // CRITICAL FIX: Use index directly as palette slot, not index * 7 diff --git a/src/app/gfx/bitmap.h b/src/app/gfx/bitmap.h index 1825198a..28f7bf81 100644 --- a/src/app/gfx/bitmap.h +++ b/src/app/gfx/bitmap.h @@ -8,6 +8,7 @@ #include #include +#include "app/gfx/backend/irenderer.h" #include "app/gfx/snes_palette.h" namespace yaze { @@ -136,19 +137,19 @@ class Bitmap { /** * @brief Creates the underlying SDL_Texture to be displayed. */ - void CreateTexture(SDL_Renderer *renderer); + void CreateTexture(); /** * @brief Updates the underlying SDL_Texture when it already exists. */ - void UpdateTexture(SDL_Renderer *renderer); + void UpdateTexture(); /** * @brief Queue texture update for batch processing (improved performance) * @param renderer SDL renderer for texture operations * @note Use this for better performance when multiple textures need updating */ - void QueueTextureUpdate(SDL_Renderer *renderer); + void QueueTextureUpdate(IRenderer *renderer); /** * @brief Updates the texture data from the surface @@ -166,6 +167,11 @@ class Bitmap { void SetPaletteWithTransparent(const SnesPalette &palette, size_t index, int length = 7); + /** + * @brief Apply the stored palette to the surface (internal helper) + */ + void ApplyStoredPalette(); + /** * @brief Set the palette using SDL colors */ @@ -251,7 +257,7 @@ class Bitmap { const uint8_t *data() const { return data_.data(); } std::vector &mutable_data() { return data_; } SDL_Surface *surface() const { return surface_; } - SDL_Texture *texture() const { return texture_; } + TextureHandle texture() const { return texture_; } const std::vector &vector() const { return data_; } uint8_t at(int i) const { return data_[i]; } bool modified() const { return modified_; } @@ -259,6 +265,7 @@ class Bitmap { void set_active(bool active) { active_ = active; } void set_data(const std::vector &data); void set_modified(bool modified) { modified_ = modified; } + void set_texture(TextureHandle texture) { texture_ = texture; } private: @@ -285,7 +292,7 @@ class Bitmap { SDL_Surface *surface_ = nullptr; // Texture for the bitmap (managed by Arena) - SDL_Texture *texture_ = nullptr; + TextureHandle texture_ = nullptr; // Optimized palette lookup cache for O(1) color index lookups std::unordered_map color_to_index_cache_; diff --git a/src/app/gfx/tilemap.cc b/src/app/gfx/tilemap.cc index 9d5ade6e..e608c482 100644 --- a/src/app/gfx/tilemap.cc +++ b/src/app/gfx/tilemap.cc @@ -2,7 +2,6 @@ #include -#include "app/core/window.h" #include "app/gfx/arena.h" #include "app/gfx/atlas_renderer.h" #include "app/gfx/bitmap.h" @@ -12,7 +11,7 @@ namespace yaze { namespace gfx { -Tilemap CreateTilemap(std::vector &data, int width, int height, +Tilemap CreateTilemap(IRenderer* renderer, std::vector &data, int width, int height, int tile_size, int num_tiles, SnesPalette &palette) { Tilemap tilemap; tilemap.tile_size.x = tile_size; @@ -21,16 +20,28 @@ Tilemap CreateTilemap(std::vector &data, int width, int height, tilemap.map_size.y = num_tiles; tilemap.atlas = Bitmap(width, height, 8, data); tilemap.atlas.SetPalette(palette); - core::Renderer::Get().RenderBitmap(&tilemap.atlas); + + // Queue texture creation directly via Arena + if (tilemap.atlas.is_active() && tilemap.atlas.surface()) { + Arena::Get().QueueTextureCommand(Arena::TextureCommandType::CREATE, &tilemap.atlas); + } + return tilemap; } -void UpdateTilemap(Tilemap &tilemap, const std::vector &data) { +void UpdateTilemap(IRenderer* renderer, Tilemap &tilemap, const std::vector &data) { tilemap.atlas.set_data(data); - core::Renderer::Get().UpdateBitmap(&tilemap.atlas); + + // Queue texture update directly via Arena + if (tilemap.atlas.texture() && tilemap.atlas.is_active() && tilemap.atlas.surface()) { + Arena::Get().QueueTextureCommand(Arena::TextureCommandType::UPDATE, &tilemap.atlas); + } else if (!tilemap.atlas.texture() && tilemap.atlas.is_active() && tilemap.atlas.surface()) { + // Create if doesn't exist yet + Arena::Get().QueueTextureCommand(Arena::TextureCommandType::CREATE, &tilemap.atlas); + } } -void RenderTile(Tilemap &tilemap, int tile_id) { +void RenderTile(IRenderer* renderer, Tilemap &tilemap, int tile_id) { // Validate tilemap state before proceeding if (!tilemap.atlas.is_active() || tilemap.atlas.vector().empty()) { return; @@ -49,7 +60,7 @@ void RenderTile(Tilemap &tilemap, int tile_id) { // Note: Tile cache disabled to prevent std::move() related crashes } -void RenderTile16(Tilemap &tilemap, int tile_id) { +void RenderTile16(IRenderer* renderer, Tilemap &tilemap, int tile_id) { // Validate tilemap state before proceeding if (!tilemap.atlas.is_active() || tilemap.atlas.vector().empty()) { return; @@ -76,7 +87,7 @@ void RenderTile16(Tilemap &tilemap, int tile_id) { // Note: Tile cache disabled to prevent std::move() related crashes } -void UpdateTile16(Tilemap &tilemap, int tile_id) { +void UpdateTile16(IRenderer* renderer, Tilemap &tilemap, int tile_id) { // Check if tile is cached Bitmap* cached_tile = tilemap.tile_cache.GetTile(tile_id); if (cached_tile) { @@ -88,10 +99,14 @@ void UpdateTile16(Tilemap &tilemap, int tile_id) { int tile_data_offset = 0; tilemap.atlas.Get16x16Tile(tile_x, tile_y, tile_data, tile_data_offset); cached_tile->set_data(tile_data); - core::Renderer::Get().UpdateBitmap(cached_tile); + + // Queue texture update directly via Arena + if (cached_tile->texture() && cached_tile->is_active()) { + Arena::Get().QueueTextureCommand(Arena::TextureCommandType::UPDATE, cached_tile); + } } else { // Tile not cached, render it fresh - RenderTile16(tilemap, tile_id); + RenderTile16(renderer, tilemap, tile_id); } } @@ -293,7 +308,7 @@ std::vector GetTilemapData(Tilemap &tilemap, int tile_id) { return data; } -void RenderTilesBatch(Tilemap& tilemap, const std::vector& tile_ids, +void RenderTilesBatch(IRenderer* renderer, Tilemap& tilemap, const std::vector& tile_ids, const std::vector>& positions, const std::vector>& scales) { if (tile_ids.empty() || positions.empty() || tile_ids.size() != positions.size()) { @@ -302,9 +317,6 @@ void RenderTilesBatch(Tilemap& tilemap, const std::vector& tile_ids, ScopedTimer timer("tilemap_batch_render"); - // Get renderer from Arena - SDL_Renderer* renderer = nullptr; // We need to get this from the renderer system - // Initialize atlas renderer if not already done auto& atlas_renderer = AtlasRenderer::Get(); if (!renderer) { @@ -340,11 +352,16 @@ void RenderTilesBatch(Tilemap& tilemap, const std::vector& tile_ids, tilemap.tile_cache.CacheTile(tile_id, std::move(new_tile)); cached_tile = tilemap.tile_cache.GetTile(tile_id); if (cached_tile) { - core::Renderer::Get().RenderBitmap(cached_tile); + cached_tile->CreateTexture(); } } if (cached_tile && cached_tile->is_active()) { + // Queue texture creation if needed + if (!cached_tile->texture() && cached_tile->surface()) { + Arena::Get().QueueTextureCommand(Arena::TextureCommandType::CREATE, cached_tile); + } + // Add to atlas renderer int atlas_id = atlas_renderer.AddBitmap(*cached_tile); if (atlas_id >= 0) { diff --git a/src/app/gfx/tilemap.h b/src/app/gfx/tilemap.h index 9fa1cb0d..2cff4a3e 100644 --- a/src/app/gfx/tilemap.h +++ b/src/app/gfx/tilemap.h @@ -2,6 +2,7 @@ #define YAZE_GFX_TILEMAP_H #include "absl/container/flat_hash_map.h" +#include "app/gfx/backend/irenderer.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_tile.h" @@ -116,15 +117,15 @@ struct Tilemap { std::vector FetchTileDataFromGraphicsBuffer( const std::vector &data, int tile_id, int sheet_offset); -Tilemap CreateTilemap(std::vector &data, int width, int height, +Tilemap CreateTilemap(IRenderer* renderer, std::vector &data, int width, int height, int tile_size, int num_tiles, SnesPalette &palette); -void UpdateTilemap(Tilemap &tilemap, const std::vector &data); +void UpdateTilemap(IRenderer* renderer, Tilemap &tilemap, const std::vector &data); -void RenderTile(Tilemap &tilemap, int tile_id); +void RenderTile(IRenderer* renderer, Tilemap &tilemap, int tile_id); -void RenderTile16(Tilemap &tilemap, int tile_id); -void UpdateTile16(Tilemap &tilemap, int tile_id); +void RenderTile16(IRenderer* renderer, Tilemap &tilemap, int tile_id); +void UpdateTile16(IRenderer* renderer, Tilemap &tilemap, int tile_id); void ModifyTile16(Tilemap &tilemap, const std::vector &data, const TileInfo &top_left, const TileInfo &top_right, @@ -146,7 +147,7 @@ std::vector GetTilemapData(Tilemap &tilemap, int tile_id); * @param scales Vector of scale factors for each tile (optional, defaults to 1.0) * @note This function uses atlas rendering to reduce draw calls significantly */ -void RenderTilesBatch(Tilemap& tilemap, const std::vector& tile_ids, +void RenderTilesBatch(IRenderer* renderer, Tilemap& tilemap, const std::vector& tile_ids, const std::vector>& positions, const std::vector>& scales = {}); diff --git a/src/app/gui/canvas.cc b/src/app/gui/canvas.cc index 706665f7..1a85cfcd 100644 --- a/src/app/gui/canvas.cc +++ b/src/app/gui/canvas.cc @@ -3,40 +3,36 @@ #include #include #include "app/gfx/bpp_format_manager.h" - -#include "app/core/window.h" -#include "app/gfx/atlas_renderer.h" #include "app/gfx/bitmap.h" #include "app/gfx/performance_profiler.h" #include "app/gui/canvas_utils.h" -#include "app/gui/color.h" #include "app/gui/style.h" #include "app/gui/canvas/canvas_automation_api.h" #include "imgui/imgui.h" -#include "imgui_memory_editor.h" -#include "util/log.h" namespace yaze::gui { -using core::Renderer; // Define constructors and destructor in .cc to avoid incomplete type issues with unique_ptr -Canvas::Canvas() { InitializeDefaults(); } +// Default constructor +Canvas::Canvas() : renderer_(nullptr) { InitializeDefaults(); } + +// Legacy constructors (renderer is optional for backward compatibility) Canvas::Canvas(const std::string& id) - : canvas_id_(id), context_id_(id + "Context") { + : renderer_(nullptr), canvas_id_(id), context_id_(id + "Context") { InitializeDefaults(); } Canvas::Canvas(const std::string& id, ImVec2 canvas_size) - : canvas_id_(id), context_id_(id + "Context") { + : renderer_(nullptr), canvas_id_(id), context_id_(id + "Context") { InitializeDefaults(); config_.canvas_size = canvas_size; config_.custom_canvas_size = true; } Canvas::Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size) - : canvas_id_(id), context_id_(id + "Context") { + : renderer_(nullptr), canvas_id_(id), context_id_(id + "Context") { InitializeDefaults(); config_.canvas_size = canvas_size; config_.custom_canvas_size = true; @@ -44,7 +40,39 @@ Canvas::Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_si } Canvas::Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size, float global_scale) - : canvas_id_(id), context_id_(id + "Context") { + : renderer_(nullptr), canvas_id_(id), context_id_(id + "Context") { + InitializeDefaults(); + config_.canvas_size = canvas_size; + config_.custom_canvas_size = true; + config_.global_scale = global_scale; + SetGridSize(grid_size); +} + +// New constructors with renderer support (for migration to IRenderer pattern) +Canvas::Canvas(gfx::IRenderer* renderer) : renderer_(renderer) { InitializeDefaults(); } + +Canvas::Canvas(gfx::IRenderer* renderer, const std::string& id) + : renderer_(renderer), canvas_id_(id), context_id_(id + "Context") { + InitializeDefaults(); +} + +Canvas::Canvas(gfx::IRenderer* renderer, const std::string& id, ImVec2 canvas_size) + : renderer_(renderer), canvas_id_(id), context_id_(id + "Context") { + InitializeDefaults(); + config_.canvas_size = canvas_size; + config_.custom_canvas_size = true; +} + +Canvas::Canvas(gfx::IRenderer* renderer, const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size) + : renderer_(renderer), canvas_id_(id), context_id_(id + "Context") { + InitializeDefaults(); + config_.canvas_size = canvas_size; + config_.custom_canvas_size = true; + SetGridSize(grid_size); +} + +Canvas::Canvas(gfx::IRenderer* renderer, const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size, float global_scale) + : renderer_(renderer), canvas_id_(id), context_id_(id + "Context") { InitializeDefaults(); config_.canvas_size = canvas_size; config_.custom_canvas_size = true; @@ -243,8 +271,7 @@ void Canvas::ShowColorAnalysis() { bool Canvas::ApplyROMPalette(int group_index, int palette_index) { if (palette_editor_ && bitmap_) { - return palette_editor_->ApplyROMPalette(bitmap_, group_index, - palette_index); + return palette_editor_->ApplyROMPalette(bitmap_, group_index, palette_index); } return false; } @@ -330,7 +357,7 @@ void Canvas::End() { // ==================== Legacy Interface ==================== -void Canvas::UpdateColorPainter(gfx::Bitmap& bitmap, const ImVec4& color, +void Canvas::UpdateColorPainter(gfx::IRenderer* renderer, gfx::Bitmap& bitmap, const ImVec4& color, const std::function& event, int tile_size, float scale) { config_.global_scale = scale; @@ -340,6 +367,7 @@ void Canvas::UpdateColorPainter(gfx::Bitmap& bitmap, const ImVec4& color, DrawBitmap(bitmap, 2, scale); if (DrawSolidTilePainter(color, tile_size)) { event(); + bitmap.UpdateTexture(); } DrawGrid(); DrawOverlay(); @@ -1796,7 +1824,7 @@ bool Canvas::ConvertBitmapFormat(gfx::BppFormat target_format) { bitmap_->set_data(converted_data); // Update the renderer - core::Renderer::Get().UpdateBitmap(bitmap_); + bitmap_->UpdateTexture(); return true; } catch (const std::exception& e) { diff --git a/src/app/gui/canvas.h b/src/app/gui/canvas.h index edab3616..7dbfafd2 100644 --- a/src/app/gui/canvas.h +++ b/src/app/gui/canvas.h @@ -53,13 +53,26 @@ enum class CanvasGridSize { k8x8, k16x16, k32x32, k64x64 }; */ class Canvas { public: + // Default constructor Canvas(); ~Canvas(); + // Legacy constructors (renderer is optional for backward compatibility) explicit Canvas(const std::string& id); explicit Canvas(const std::string& id, ImVec2 canvas_size); explicit Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size); explicit Canvas(const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size, float global_scale); + + // New constructors with renderer support (for migration to IRenderer pattern) + explicit Canvas(gfx::IRenderer* renderer); + explicit Canvas(gfx::IRenderer* renderer, const std::string& id); + explicit Canvas(gfx::IRenderer* renderer, const std::string& id, ImVec2 canvas_size); + explicit Canvas(gfx::IRenderer* renderer, const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size); + explicit Canvas(gfx::IRenderer* renderer, const std::string& id, ImVec2 canvas_size, CanvasGridSize grid_size, float global_scale); + + // Set renderer after construction (for late initialization) + void SetRenderer(gfx::IRenderer* renderer) { renderer_ = renderer; } + gfx::IRenderer* renderer() const { return renderer_; } void SetGridSize(CanvasGridSize grid_size) { switch (grid_size) { @@ -89,7 +102,7 @@ class Canvas { return CanvasGridSize::k16x16; // Default } - void UpdateColorPainter(gfx::Bitmap &bitmap, const ImVec4 &color, + void UpdateColorPainter(gfx::IRenderer* renderer, gfx::Bitmap &bitmap, const ImVec4 &color, const std::function &event, int tile_size, float scale = 1.0f); @@ -248,20 +261,6 @@ class Canvas { bool WasDoubleClicked(ImGuiMouseButton button = ImGuiMouseButton_Left) const; ImVec2 GetLastClickPosition() const; - private: - void DrawContextMenuItem(const ContextMenuItem& item); - - // Tile painter shows a preview of the currently selected tile - // and allows the user to left click to paint the tile or right - // click to select a new tile to paint with. - // (Moved to public section) - - // Draws a tile on the canvas at the specified position - // (Moved to public section) - - // These methods are now public - see public section above - - public: // Tile painter methods bool DrawTilePainter(const Bitmap &bitmap, int size, float scale = 1.0f); bool DrawSolidTilePainter(const ImVec4 &color, int size); @@ -401,7 +400,10 @@ class Canvas { Rom *rom() const { return rom_; } private: + void DrawContextMenuItem(const ContextMenuItem& item); + // Modular configuration and state + gfx::IRenderer* renderer_ = nullptr; CanvasConfig config_; CanvasSelection selection_; std::unique_ptr palette_editor_; @@ -506,13 +508,21 @@ void TableCanvasPipeline(gui::Canvas &canvas, gfx::Bitmap &bitmap, class ScopedCanvas { public: /** - * @brief Construct and begin a new canvas + * @brief Construct and begin a new canvas (legacy constructor without renderer) */ explicit ScopedCanvas(const std::string& id, ImVec2 canvas_size = ImVec2(0, 0)) : canvas_(new Canvas(id, canvas_size)), owned_(true), active_(true) { canvas_->Begin(); } + /** + * @brief Construct and begin a new canvas (with optional renderer) + */ + explicit ScopedCanvas(gfx::IRenderer* renderer, const std::string& id, ImVec2 canvas_size = ImVec2(0, 0)) + : canvas_(new Canvas(renderer, id, canvas_size)), owned_(true), active_(true) { + canvas_->Begin(); + } + /** * @brief Wrap existing canvas with RAII */ diff --git a/src/app/gui/canvas/canvas_context_menu.cc b/src/app/gui/canvas/canvas_context_menu.cc index e7f0ea0f..eb926b50 100644 --- a/src/app/gui/canvas/canvas_context_menu.cc +++ b/src/app/gui/canvas/canvas_context_menu.cc @@ -1,14 +1,9 @@ #include "canvas_context_menu.h" -#include -#include -#include - -#include "app/core/window.h" +#include "app/gfx/arena.h" #include "app/gfx/performance_profiler.h" #include "app/gfx/performance_dashboard.h" #include "app/gui/widgets/palette_widget.h" -#include "app/gui/bpp_format_ui.h" #include "app/gui/icons.h" #include "app/gui/color.h" #include "app/gui/canvas/canvas_modals.h" @@ -303,15 +298,21 @@ void CanvasContextMenu::RenderBitmapOperationsMenu(gfx::Bitmap* bitmap) { if (ImGui::BeginMenu("Format")) { if (ImGui::MenuItem("Indexed")) { bitmap->Reformat(gfx::BitmapFormat::kIndexed); - core::Renderer::Get().UpdateBitmap(bitmap); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, bitmap); } if (ImGui::MenuItem("4BPP")) { bitmap->Reformat(gfx::BitmapFormat::k4bpp); - core::Renderer::Get().UpdateBitmap(bitmap); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, bitmap); } if (ImGui::MenuItem("8BPP")) { bitmap->Reformat(gfx::BitmapFormat::k8bpp); - core::Renderer::Get().UpdateBitmap(bitmap); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, bitmap); } ImGui::EndMenu(); } diff --git a/src/app/gui/canvas_utils.cc b/src/app/gui/canvas_utils.cc index e108efee..69137ea4 100644 --- a/src/app/gui/canvas_utils.cc +++ b/src/app/gui/canvas_utils.cc @@ -1,7 +1,7 @@ #include "canvas_utils.h" #include -#include "app/core/window.h" +#include "app/gfx/arena.h" #include "app/gfx/snes_palette.h" #include "util/log.h" @@ -9,8 +9,6 @@ namespace yaze { namespace gui { namespace CanvasUtils { -using core::Renderer; - ImVec2 AlignToGrid(ImVec2 pos, float grid_step) { return ImVec2(std::floor(pos.x / grid_step) * grid_step, std::floor(pos.y / grid_step) * grid_step); @@ -95,35 +93,30 @@ bool LoadROMPaletteGroups(Rom* rom, CanvasPaletteManager& palette_manager) { } } -bool ApplyPaletteGroup(gfx::Bitmap* bitmap, - const CanvasPaletteManager& palette_manager, +bool ApplyPaletteGroup(gfx::IRenderer* renderer, gfx::Bitmap* bitmap, const CanvasPaletteManager& palette_manager, int group_index, int palette_index) { - if (!bitmap || group_index < 0 || - group_index >= - static_cast(palette_manager.rom_palette_groups.size())) { + if (!bitmap) return false; + + if (group_index < 0 || group_index >= palette_manager.rom_palette_groups.size()) { return false; } - try { - const auto& selected_palette = - palette_manager.rom_palette_groups[group_index]; - - // Apply the palette based on the index - if (palette_index >= 0 && palette_index < 8) { - bitmap->SetPaletteWithTransparent(selected_palette, palette_index); - } else { - bitmap->SetPalette(selected_palette); - } - - Renderer::Get().UpdateBitmap(bitmap); - LOG_DEBUG("Canvas", "Applied palette group %d, index %d to bitmap", - group_index, palette_index); - return true; - - } catch (const std::exception& e) { - LOG_ERROR("Canvas", "Failed to apply palette"); - return false; + const auto& palette = palette_manager.rom_palette_groups[group_index]; + + // Apply the full palette or use SetPaletteWithTransparent if palette_index is specified + if (palette_index == 0) { + bitmap->SetPalette(palette); + } else { + bitmap->SetPaletteWithTransparent(palette, palette_index); } + bitmap->set_modified(true); + + // Queue texture update via Arena's deferred system + if (renderer) { + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, bitmap); + } + return true; } // Drawing utility functions diff --git a/src/app/gui/canvas_utils.h b/src/app/gui/canvas_utils.h index 55868d31..16cb7ab5 100644 --- a/src/app/gui/canvas_utils.h +++ b/src/app/gui/canvas_utils.h @@ -89,7 +89,7 @@ int GetTileIdFromPosition(ImVec2 mouse_pos, float tile_size, float scale, int ti // Palette management utilities bool LoadROMPaletteGroups(Rom* rom, CanvasPaletteManager& palette_manager); -bool ApplyPaletteGroup(gfx::Bitmap* bitmap, const CanvasPaletteManager& palette_manager, +bool ApplyPaletteGroup(gfx::IRenderer* renderer, gfx::Bitmap* bitmap, const CanvasPaletteManager& palette_manager, int group_index, int palette_index); // Drawing utility functions (moved from Canvas class) diff --git a/src/app/gui/input.cc b/src/app/gui/input.cc index fdcb9cdc..78dc51f2 100644 --- a/src/app/gui/input.cc +++ b/src/app/gui/input.cc @@ -28,6 +28,12 @@ static inline ImGuiInputTextFlags InputScalar_DefaultCharsFilter( ? ImGuiInputTextFlags_CharsHexadecimal : ImGuiInputTextFlags_CharsDecimal; } + +// Helper: returns true if label is "invisible" (starts with "##") +static inline bool IsInvisibleLabel(const char* label) { + return label && label[0] == '#' && label[1] == '#'; +} + bool InputScalarLeft(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, float input_width, @@ -52,22 +58,24 @@ bool InputScalarLeft(const char* label, ImGuiDataType data_type, void* p_data, flags |= ImGuiInputTextFlags_AutoSelectAll; bool value_changed = false; - // if (p_step == NULL) { - // ImGui::SetNextItemWidth(input_width); - // if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) - // value_changed = DataTypeApplyFromText(buf, data_type, p_data, format); - // } else { const float button_size = GetFrameHeight(); - AlignTextToFramePadding(); - Text("%s", label); - SameLine(); + + // Support invisible labels (##) by not rendering the label, but still using it for ID + bool invisible_label = IsInvisibleLabel(label); + + if (!invisible_label) { + AlignTextToFramePadding(); + Text("%s", label); + SameLine(); + } + BeginGroup(); // The only purpose of the group here is to allow the caller // to query item data e.g. IsItemActive() PushID(label); SetNextItemWidth(ImMax( 1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2)); - // Place the label on the left of the input field + // Place the label on the left of the input field, unless invisible PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2{style.ItemSpacing.x, style.ItemSpacing.y}); PushStyleVar(ImGuiStyleVar_FramePadding, diff --git a/src/app/gui/style.cc b/src/app/gui/style.cc index 6b5dc09e..a45ef34b 100644 --- a/src/app/gui/style.cc +++ b/src/app/gui/style.cc @@ -5,7 +5,7 @@ #include "util/file_util.h" #include "app/gui/theme_manager.h" #include "app/editor/ui/background_renderer.h" -#include "app/core/platform/font_loader.h" +#include "app/platform/font_loader.h" #include "app/gui/color.h" #include "app/gui/icons.h" #include "imgui/imgui.h" @@ -1293,7 +1293,7 @@ void DrawFontManager() { static bool font_selected = false; ImGui::Text("Loaded fonts"); - for (const auto &loaded_font : core::font_registry.fonts) { + for (const auto &loaded_font : font_registry.fonts) { ImGui::Text("%s", loaded_font.font_path); } ImGui::Separator(); diff --git a/src/app/gui/widgets/agent_chat_widget.cc b/src/app/gui/widgets/agent_chat_widget.cc index e5dd2a1e..619f662f 100644 --- a/src/app/gui/widgets/agent_chat_widget.cc +++ b/src/app/gui/widgets/agent_chat_widget.cc @@ -14,7 +14,7 @@ #endif namespace yaze { -namespace app { + namespace gui { AgentChatWidget::AgentChatWidget() @@ -334,5 +334,5 @@ void AgentChatWidget::ScrollToBottom() { } } // namespace gui -} // namespace app + } // namespace yaze diff --git a/src/app/gui/widgets/agent_chat_widget.h b/src/app/gui/widgets/agent_chat_widget.h index 52d130b8..3703dd7b 100644 --- a/src/app/gui/widgets/agent_chat_widget.h +++ b/src/app/gui/widgets/agent_chat_widget.h @@ -10,7 +10,7 @@ #include "app/rom.h" namespace yaze { -namespace app { + namespace gui { /** @@ -77,7 +77,7 @@ class AgentChatWidget { }; } // namespace gui -} // namespace app + } // namespace yaze #endif // YAZE_APP_GUI_WIDGETS_AGENT_CHAT_WIDGET_H_ diff --git a/src/app/gui/widgets/collaboration_panel.cc b/src/app/gui/widgets/collaboration_panel.cc index c25d03e6..219b69ed 100644 --- a/src/app/gui/widgets/collaboration_panel.cc +++ b/src/app/gui/widgets/collaboration_panel.cc @@ -8,7 +8,7 @@ #include "imgui/imgui.h" namespace yaze { -namespace app { + namespace gui { CollaborationPanel::CollaborationPanel() @@ -660,5 +660,5 @@ void CollaborationPanel::RenderApprovalProposal( } } // namespace gui -} // namespace app + } // namespace yaze diff --git a/src/app/gui/widgets/collaboration_panel.h b/src/app/gui/widgets/collaboration_panel.h index c5a03001..790c1135 100644 --- a/src/app/gui/widgets/collaboration_panel.h +++ b/src/app/gui/widgets/collaboration_panel.h @@ -15,7 +15,7 @@ #endif namespace yaze { -namespace app { + namespace gui { /** @@ -180,7 +180,7 @@ class CollaborationPanel { }; } // namespace gui -} // namespace app + } // namespace yaze #endif // YAZE_APP_GUI_WIDGETS_COLLABORATION_PANEL_H_ diff --git a/src/app/gui/widgets/dungeon_object_emulator_preview.cc b/src/app/gui/widgets/dungeon_object_emulator_preview.cc index a3f9fcb6..7094c5f7 100644 --- a/src/app/gui/widgets/dungeon_object_emulator_preview.cc +++ b/src/app/gui/widgets/dungeon_object_emulator_preview.cc @@ -1,4 +1,5 @@ #include "app/gui/widgets/dungeon_object_emulator_preview.h" +#include "app/gfx/backend/irenderer.h" #include "app/zelda3/dungeon/room.h" #include "app/zelda3/dungeon/room_object.h" @@ -11,23 +12,22 @@ namespace gui { DungeonObjectEmulatorPreview::DungeonObjectEmulatorPreview() { snes_instance_ = std::make_unique(); - object_texture_ = SDL_CreateTexture(core::Renderer::Get().renderer(), - SDL_PIXELFORMAT_ARGB8888, - SDL_TEXTUREACCESS_STREAMING, 256, 256); } DungeonObjectEmulatorPreview::~DungeonObjectEmulatorPreview() { - if (object_texture_) { - SDL_DestroyTexture(object_texture_); - } + // if (object_texture_) { + // renderer_->DestroyTexture(object_texture_); + // } } -void DungeonObjectEmulatorPreview::Initialize(Rom* rom) { - rom_ = rom; - if (rom_ && rom_->is_loaded()) { - auto rom_data = rom_->vector(); - snes_instance_->Init(rom_data); - } +void DungeonObjectEmulatorPreview::Initialize(gfx::IRenderer* renderer, Rom* rom) { + renderer_ = renderer; + rom_ = rom; + snes_instance_ = std::make_unique(); + std::vector rom_data = rom->vector(); + snes_instance_->Init(rom_data); + + // object_texture_ = renderer_->CreateTexture(256, 256); } void DungeonObjectEmulatorPreview::Render() { @@ -286,9 +286,9 @@ void DungeonObjectEmulatorPreview::TriggerEmulatedRender() { // 15. Get the rendered pixels from PPU void* pixels = nullptr; int pitch = 0; - if (SDL_LockTexture(object_texture_, nullptr, &pixels, &pitch) == 0) { + if (renderer_->LockTexture(object_texture_, nullptr, &pixels, &pitch)) { snes_instance_->SetPixels(static_cast(pixels)); - SDL_UnlockTexture(object_texture_); + renderer_->UnlockTexture(object_texture_); } } diff --git a/src/app/gui/widgets/dungeon_object_emulator_preview.h b/src/app/gui/widgets/dungeon_object_emulator_preview.h index 78bdaf35..4fd9fdf8 100644 --- a/src/app/gui/widgets/dungeon_object_emulator_preview.h +++ b/src/app/gui/widgets/dungeon_object_emulator_preview.h @@ -4,6 +4,12 @@ #include "app/emu/snes.h" #include "app/rom.h" +namespace yaze { +namespace gfx { +class IRenderer; +} // namespace gfx +} + namespace yaze { namespace gui { @@ -12,16 +18,17 @@ class DungeonObjectEmulatorPreview { DungeonObjectEmulatorPreview(); ~DungeonObjectEmulatorPreview(); - void Initialize(Rom* rom); + void Initialize(gfx::IRenderer* renderer, Rom* rom); void Render(); private: void RenderControls(); void TriggerEmulatedRender(); + gfx::IRenderer* renderer_ = nullptr; Rom* rom_ = nullptr; std::unique_ptr snes_instance_; - SDL_Texture* object_texture_ = nullptr; + void* object_texture_ = nullptr; int object_id_ = 0; int room_id_ = 0; diff --git a/src/app/gui/widgets/palette_widget.cc b/src/app/gui/widgets/palette_widget.cc index e42282fc..16d99361 100644 --- a/src/app/gui/widgets/palette_widget.cc +++ b/src/app/gui/widgets/palette_widget.cc @@ -2,15 +2,13 @@ #include #include -#include "app/core/window.h" +#include "app/gfx/arena.h" #include "app/gui/color.h" #include "util/log.h" namespace yaze { namespace gui { -using core::Renderer; - void PaletteWidget::Initialize(Rom* rom) { rom_ = rom; rom_palettes_loaded_ = false; @@ -169,7 +167,9 @@ bool PaletteWidget::ApplyROMPalette(gfx::Bitmap* bitmap, int group_index, int pa bitmap->SetPalette(selected_palette); } - Renderer::Get().UpdateBitmap(bitmap); + // Queue texture update via Arena's deferred system + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, bitmap); current_group_index_ = group_index; current_palette_index_ = palette_index; diff --git a/src/app/main.cc b/src/app/main.cc index 1f94f2bc..8261a1fc 100644 --- a/src/app/main.cc +++ b/src/app/main.cc @@ -1,5 +1,5 @@ #if __APPLE__ -#include "app/core/platform/app_delegate.h" +#include "app/platform/app_delegate.h" #endif #define IMGUI_DEFINE_MATH_OPERATORS diff --git a/src/app/net/collaboration_service.cc b/src/app/net/collaboration_service.cc index 9310e547..40e919b2 100644 --- a/src/app/net/collaboration_service.cc +++ b/src/app/net/collaboration_service.cc @@ -6,7 +6,7 @@ #include "absl/strings/str_format.h" namespace yaze { -namespace app { + namespace net { CollaborationService::CollaborationService(Rom* rom) @@ -436,5 +436,5 @@ bool CollaborationService::ShouldAutoSync() { } } // namespace net -} // namespace app + } // namespace yaze diff --git a/src/app/net/collaboration_service.h b/src/app/net/collaboration_service.h index a6740589..19932af2 100644 --- a/src/app/net/collaboration_service.h +++ b/src/app/net/collaboration_service.h @@ -11,7 +11,7 @@ #include "app/rom.h" namespace yaze { -namespace app { + namespace net { /** @@ -160,7 +160,7 @@ class CollaborationService { }; } // namespace net -} // namespace app + } // namespace yaze #endif // YAZE_APP_NET_COLLABORATION_SERVICE_H_ diff --git a/src/app/net/rom_service_impl.cc b/src/app/net/rom_service_impl.cc index 45d46cd9..a0123f06 100644 --- a/src/app/net/rom_service_impl.cc +++ b/src/app/net/rom_service_impl.cc @@ -10,7 +10,7 @@ namespace rom_svc = ::yaze::proto; namespace yaze { -namespace app { + namespace net { RomServiceImpl::RomServiceImpl( @@ -204,7 +204,7 @@ grpc::Status RomServiceImpl::SetDialogue( } } // namespace net -} // namespace app + } // namespace yaze #endif // YAZE_WITH_GRPC \ No newline at end of file diff --git a/src/app/net/rom_service_impl.h b/src/app/net/rom_service_impl.h index 9a7b3ec0..dae19b2f 100644 --- a/src/app/net/rom_service_impl.h +++ b/src/app/net/rom_service_impl.h @@ -17,7 +17,7 @@ #include "app/net/rom_version_manager.h" namespace yaze { -namespace app { + namespace net { #ifdef YAZE_WITH_GRPC @@ -58,7 +58,7 @@ class RomServiceImpl final : public proto::RomService::Service { ~RomServiceImpl() override = default; // Initialize with configuration - void SetConfig(const Config& config) { config_ = config; } + void SetConfig(const Config& config); // ========================================================================= // Basic ROM Operations @@ -165,7 +165,7 @@ class RomServiceImpl final : public proto::RomService::Service { #endif // YAZE_WITH_GRPC } // namespace net -} // namespace app + } // namespace yaze #endif // YAZE_APP_NET_ROM_SERVICE_IMPL_H_ diff --git a/src/app/net/rom_version_manager.cc b/src/app/net/rom_version_manager.cc index 2a31fc9d..56236161 100644 --- a/src/app/net/rom_version_manager.cc +++ b/src/app/net/rom_version_manager.cc @@ -15,7 +15,7 @@ #endif namespace yaze { -namespace app { + namespace net { namespace { @@ -529,5 +529,5 @@ ProposalApprovalManager::GetProposalStatus( } } // namespace net -} // namespace app + } // namespace yaze diff --git a/src/app/net/rom_version_manager.h b/src/app/net/rom_version_manager.h index 7902d793..6a2cb75a 100644 --- a/src/app/net/rom_version_manager.h +++ b/src/app/net/rom_version_manager.h @@ -15,7 +15,7 @@ #endif namespace yaze { -namespace app { + namespace net { /** @@ -286,7 +286,7 @@ class ProposalApprovalManager { }; } // namespace net -} // namespace app + } // namespace yaze #endif // YAZE_APP_NET_ROM_VERSION_MANAGER_H_ diff --git a/src/app/net/websocket_client.cc b/src/app/net/websocket_client.cc index 0dd4e205..48f17390 100644 --- a/src/app/net/websocket_client.cc +++ b/src/app/net/websocket_client.cc @@ -14,7 +14,7 @@ #endif namespace yaze { -namespace app { + namespace net { #ifdef YAZE_WITH_JSON @@ -460,5 +460,5 @@ absl::Status WebSocketClient::SendRaw(const nlohmann::json& message) { } } // namespace net -} // namespace app + } // namespace yaze diff --git a/src/app/net/websocket_client.h b/src/app/net/websocket_client.h index d6bd72dd..c6ac4044 100644 --- a/src/app/net/websocket_client.h +++ b/src/app/net/websocket_client.h @@ -16,7 +16,7 @@ #endif namespace yaze { -namespace app { + namespace net { /** @@ -208,7 +208,7 @@ class WebSocketClient { }; } // namespace net -} // namespace app + } // namespace yaze #endif // YAZE_APP_NET_WEBSOCKET_CLIENT_H_ diff --git a/src/app/core/platform/app_delegate.h b/src/app/platform/app_delegate.h similarity index 92% rename from src/app/core/platform/app_delegate.h rename to src/app/platform/app_delegate.h index a1e4aa56..a9df15a9 100644 --- a/src/app/core/platform/app_delegate.h +++ b/src/app/platform/app_delegate.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_CORE_PLATFORM_APP_DELEGATE_H -#define YAZE_APP_CORE_PLATFORM_APP_DELEGATE_H +#ifndef YAZE_APP_PLATFORM_APP_DELEGATE_H +#define YAZE_APP_PLATFORM_APP_DELEGATE_H #if defined(__APPLE__) && defined(__MACH__) /* Apple OSX and iOS (Darwin). */ @@ -61,4 +61,4 @@ int yaze_run_cocoa_app_delegate(const char *filename); #endif // defined(__APPLE__) && defined(__MACH__) -#endif // YAZE_APP_CORE_PLATFORM_APP_DELEGATE_H +#endif // YAZE_APP_PLATFORM_APP_DELEGATE_H diff --git a/src/app/core/platform/app_delegate.mm b/src/app/platform/app_delegate.mm similarity index 99% rename from src/app/core/platform/app_delegate.mm rename to src/app/platform/app_delegate.mm index 8b3b51bc..f749d81a 100644 --- a/src/app/core/platform/app_delegate.mm +++ b/src/app/platform/app_delegate.mm @@ -1,5 +1,5 @@ // AppDelegate.mm -#import "app/core/platform/app_delegate.h" +#import "app/platform/app_delegate.h" #import "app/core/controller.h" #import "util/file_util.h" #import "app/editor/editor.h" diff --git a/src/app/core/platform/asset_loader.cc b/src/app/platform/asset_loader.cc similarity index 97% rename from src/app/core/platform/asset_loader.cc rename to src/app/platform/asset_loader.cc index 1c89d629..1e0bd0fb 100644 --- a/src/app/core/platform/asset_loader.cc +++ b/src/app/platform/asset_loader.cc @@ -1,4 +1,4 @@ -#include "app/core/platform/asset_loader.h" +#include "app/platform/asset_loader.h" #include #include @@ -7,7 +7,7 @@ #include "util/file_util.h" namespace yaze { -namespace core { + std::vector AssetLoader::GetSearchPaths(const std::string& relative_path) { std::vector search_paths; @@ -90,5 +90,5 @@ bool AssetLoader::AssetExists(const std::string& relative_path) { return FindAssetFile(relative_path).ok(); } -} // namespace core + } // namespace yaze diff --git a/src/app/core/platform/asset_loader.h b/src/app/platform/asset_loader.h similarity index 88% rename from src/app/core/platform/asset_loader.h rename to src/app/platform/asset_loader.h index 4d52b722..d4044d6a 100644 --- a/src/app/core/platform/asset_loader.h +++ b/src/app/platform/asset_loader.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_CORE_PLATFORM_ASSET_LOADER_H_ -#define YAZE_APP_CORE_PLATFORM_ASSET_LOADER_H_ +#ifndef YAZE_APP_PLATFORM_ASSET_LOADER_H_ +#define YAZE_APP_PLATFORM_ASSET_LOADER_H_ #include #include @@ -8,7 +8,7 @@ #include "absl/status/statusor.h" namespace yaze { -namespace core { + /** * @class AssetLoader @@ -51,7 +51,7 @@ class AssetLoader { static bool AssetExists(const std::string& relative_path); }; -} // namespace core + } // namespace yaze -#endif // YAZE_APP_CORE_PLATFORM_ASSET_LOADER_H_ +#endif // YAZE_APP_PLATFORM_ASSET_LOADER_H_ diff --git a/src/app/core/platform/file_dialog.mm b/src/app/platform/file_dialog.mm similarity index 99% rename from src/app/core/platform/file_dialog.mm rename to src/app/platform/file_dialog.mm index d97655bb..01e79a95 100644 --- a/src/app/core/platform/file_dialog.mm +++ b/src/app/platform/file_dialog.mm @@ -22,7 +22,7 @@ #import #import -#include "app/core/platform/app_delegate.h" +#include "app/platform/app_delegate.h" namespace { static std::string selectedFile; diff --git a/src/app/core/platform/font_loader.cc b/src/app/platform/font_loader.cc similarity index 95% rename from src/app/core/platform/font_loader.cc rename to src/app/platform/font_loader.cc index 4f0fc7f6..3d6ca4ba 100644 --- a/src/app/core/platform/font_loader.cc +++ b/src/app/platform/font_loader.cc @@ -1,4 +1,4 @@ -#include "app/core/platform/font_loader.h" +#include "app/platform/font_loader.h" #include #include @@ -15,7 +15,6 @@ #include "util/macro.h" namespace yaze { -namespace core { static const char* KARLA_REGULAR = "Karla-Regular.ttf"; static const char* ROBOTO_MEDIUM = "Roboto-Medium.ttf"; @@ -138,5 +137,4 @@ void LoadSystemFonts() { } #endif -} // namespace core } // namespace yaze diff --git a/src/app/core/platform/font_loader.h b/src/app/platform/font_loader.h similarity index 72% rename from src/app/core/platform/font_loader.h rename to src/app/platform/font_loader.h index d18a28fe..e55186e9 100644 --- a/src/app/core/platform/font_loader.h +++ b/src/app/platform/font_loader.h @@ -1,5 +1,5 @@ -#ifndef YAZE_APP_CORE_PLATFORM_FONTLOADER_H -#define YAZE_APP_CORE_PLATFORM_FONTLOADER_H +#ifndef YAZE_APP_PLATFORM_FONTLOADER_H +#define YAZE_APP_PLATFORM_FONTLOADER_H #include @@ -7,7 +7,7 @@ #include "imgui/imgui.h" namespace yaze { -namespace core { + struct FontConfig { const char* font_path; @@ -28,7 +28,7 @@ absl::Status ReloadPackageFont(const FontConfig& config); void LoadSystemFonts(); -} // namespace core + } // namespace yaze -#endif // YAZE_APP_CORE_PLATFORM_FONTLOADER_H +#endif // YAZE_APP_PLATFORM_FONTLOADER_H diff --git a/src/app/core/platform/font_loader.mm b/src/app/platform/font_loader.mm similarity index 93% rename from src/app/core/platform/font_loader.mm rename to src/app/platform/font_loader.mm index 04e63efc..0a4d1321 100644 --- a/src/app/core/platform/font_loader.mm +++ b/src/app/platform/font_loader.mm @@ -1,4 +1,4 @@ -#include "app/core/platform/font_loader.h" +#include "app/platform/font_loader.h" #import #include @@ -8,13 +8,13 @@ #if TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1 /* iOS */ -void yaze::core::LoadSystemFonts() {} +void yaze::LoadSystemFonts() {} #elif TARGET_OS_MAC == 1 /* macOS */ #import -void yaze::core::LoadSystemFonts() { +void yaze::LoadSystemFonts() { NSArray *fontNames = @[ @"Helvetica", @"Times New Roman", @"Courier", @"Arial", @"Verdana" ]; for (NSString *fontName in fontNames) { diff --git a/src/app/core/platform/view_controller.h b/src/app/platform/view_controller.h similarity index 100% rename from src/app/core/platform/view_controller.h rename to src/app/platform/view_controller.h diff --git a/src/app/rom.cc b/src/app/rom.cc index 6f9b60ca..656cec26 100644 --- a/src/app/rom.cc +++ b/src/app/rom.cc @@ -105,10 +105,8 @@ absl::StatusOr> LoadLinkGraphics( link_graphics[i].Create(gfx::kTilesheetWidth, gfx::kTilesheetHeight, gfx::kTilesheetDepth, link_sheet_8bpp); link_graphics[i].SetPalette(rom.palette_group().armors[0]); - // TODO: Renderer refactor to use IRenderer or defer for later when GraphicsEditor is opened. - // if (SDL_Renderer *renderer = Renderer::Get().renderer(); renderer != nullptr) { - // Renderer::Get().RenderBitmap(&link_graphics[i]); - // } + // Texture creation is deferred until GraphicsEditor is opened and renderer is available. + // The graphics will be queued for texturing when needed via Arena's deferred system. } return link_graphics; } diff --git a/src/app/zelda3/dungeon/room.cc b/src/app/zelda3/dungeon/room.cc index 91da9ff5..47101110 100644 --- a/src/app/zelda3/dungeon/room.cc +++ b/src/app/zelda3/dungeon/room.cc @@ -321,8 +321,9 @@ void Room::RenderRoomGraphics() { } // CRITICAL: Recreate textures with the palette applied! - core::Renderer::Get().RenderBitmap(&bg1_buffer_.bitmap()); - core::Renderer::Get().RenderBitmap(&bg2_buffer_.bitmap()); + // TODO: Queue texture for later rendering. + // core::Renderer::Get().RenderBitmap(&bg1_buffer_.bitmap()); + // core::Renderer::Get().RenderBitmap(&bg2_buffer_.bitmap()); } void Room::RenderObjectsToBackground() { diff --git a/src/app/zelda3/screen/dungeon_map.cc b/src/app/zelda3/screen/dungeon_map.cc index 7b94b3d4..98a1db2a 100644 --- a/src/app/zelda3/screen/dungeon_map.cc +++ b/src/app/zelda3/screen/dungeon_map.cc @@ -8,6 +8,7 @@ #include "app/gfx/bitmap.h" #include "app/gfx/snes_tile.h" #include "app/gfx/tilemap.h" +#include "app/gfx/backend/irenderer.h" #include "app/snes.h" #include "util/hex.h" @@ -130,7 +131,8 @@ absl::Status LoadDungeonMapTile16(gfx::Tilemap &tile16_blockset, Rom &rom, } tile16_blockset.atlas.SetPalette(*rom.mutable_dungeon_palette(3)); - core::Renderer::Get().RenderBitmap(&tile16_blockset.atlas); + // TODO: Queue texture for later rendering. + // core::Renderer::Get().RenderBitmap(&tile16_blockset.atlas); return absl::OkStatus(); } @@ -187,7 +189,8 @@ absl::Status LoadDungeonMapGfxFromBinary(Rom &rom, converted_bin.begin() + ((i + 1) * 0x1000)); sheets[i] = gfx::Bitmap(128, 32, 8, gfx_sheets[i]); sheets[i].SetPalette(*rom.mutable_dungeon_palette(3)); - core::Renderer::Get().RenderBitmap(&sheets[i]); + // TODO: Queue texture for later rendering. + // core::Renderer::Get().RenderBitmap(&sheets[i]); } } file.close(); diff --git a/src/app/zelda3/screen/inventory.cc b/src/app/zelda3/screen/inventory.cc index f648f2c9..ce7ae39b 100644 --- a/src/app/zelda3/screen/inventory.cc +++ b/src/app/zelda3/screen/inventory.cc @@ -1,5 +1,6 @@ #include "inventory.h" +#include "app/gfx/backend/irenderer.h" #include "app/core/window.h" #include "app/gfx/bitmap.h" #include "app/gfx/snes_tile.h" @@ -8,8 +9,6 @@ namespace yaze { namespace zelda3 { -using core::Renderer; - absl::Status Inventory::Create() { data_.reserve(256 * 256); for (int i = 0; i < 256 * 256; i++) { @@ -69,7 +68,8 @@ absl::Status Inventory::Create() { bitmap_.Create(256, 256, 8, data_); bitmap_.SetPalette(palette_); - Renderer::Get().RenderBitmap(&bitmap_); + // TODO: Queue texture for later rendering. + // Renderer::Get().RenderBitmap(&bitmap_); return absl::OkStatus(); } @@ -88,7 +88,8 @@ absl::Status Inventory::BuildTileset() { auto hud_pal_group = rom()->palette_group().hud; palette_ = hud_pal_group[0]; tilesheets_bmp_.SetPalette(palette_); - Renderer::Get().RenderBitmap(&tilesheets_bmp_); + // TODO: Queue texture for later rendering. + // Renderer::Get().RenderBitmap(&tilesheets_bmp_); return absl::OkStatus(); } diff --git a/src/cli/service/agent/tool_dispatcher.h b/src/cli/service/agent/tool_dispatcher.h index 3ae2f13b..8a788e1e 100644 --- a/src/cli/service/agent/tool_dispatcher.h +++ b/src/cli/service/agent/tool_dispatcher.h @@ -12,6 +12,38 @@ class Rom; namespace cli { namespace agent { +enum class ToolCallType { + kUnknown, + kResourceList, + kResourceSearch, + kDungeonListSprites, + kDungeonDescribeRoom, + // Overworld + kOverworldFindTile, + kOverworldDescribeMap, + kOverworldListWarps, + kOverworldListSprites, + kOverworldGetEntrance, + kOverworldTileStats, + // + kMessageList, + kMessageRead, + kMessageSearch, + kGuiPlaceTile, + kGuiClick, + kGuiDiscover, + kGuiScreenshot, + kDialogueList, + kDialogueRead, + kDialogueSearch, + kMusicList, + kMusicInfo, + kMusicTracks, + kSpriteList, + kSpriteProperties, + kSpritePalette, +}; + class ToolDispatcher { public: ToolDispatcher() = default; diff --git a/src/ios/main.mm b/src/ios/main.mm index 183d16bc..c9939c7d 100644 --- a/src/ios/main.mm +++ b/src/ios/main.mm @@ -32,8 +32,8 @@ #include #include "app/core/controller.h" -#include "app/core/platform/app_delegate.h" -#include "app/core/platform/font_loader.h" +#include "app/platform/app_delegate.h" +#include "app/platform/font_loader.h" #include "app/core/window.h" #include "app/rom.h" @@ -43,7 +43,7 @@ #undef main #endif -#include "app/core/platform/view_controller.h" +#include "app/platform/view_controller.h" #include "imgui/backends/imgui_impl_sdl2.h" #include "imgui/backends/imgui_impl_sdlrenderer2.h" #include "imgui/imgui.h" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 118f41c7..452f26f9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -12,150 +12,95 @@ foreach (file endforeach() # Only build test executable if tests are enabled -# Double-check to ensure tests are actually enabled if(YAZE_BUILD_TESTS AND NOT YAZE_BUILD_TESTS STREQUAL "OFF") - # Main test executable with enhanced argument handling for AI agents - # Use CI version for minimal builds, full version for development + + # Base list of test sources for all builds + set(YAZE_TEST_BASE_SOURCES + test_editor.cc + test_editor.h + testing.h + test_utils.h + + # Unit Tests + unit/core/asar_wrapper_test.cc + unit/core/hex_test.cc + unit/cli/resource_catalog_test.cc + unit/rom/rom_test.cc + unit/gfx/snes_tile_test.cc + unit/gfx/compression_test.cc + unit/gfx/snes_palette_test.cc + unit/gui/tile_selector_widget_test.cc + unit/gui/canvas_automation_api_test.cc + unit/zelda3/overworld_test.cc + unit/zelda3/object_parser_test.cc + unit/zelda3/object_parser_structs_test.cc + unit/zelda3/sprite_builder_test.cc + unit/zelda3/test_dungeon_objects.cc + unit/zelda3/dungeon_component_unit_test.cc + unit/zelda3/dungeon/room_object_encoding_test.cc + unit/zelda3/dungeon/room_manipulation_test.cc + unit/zelda3/dungeon_object_renderer_mock_test.cc + + # CLI Services (for catalog serialization tests) + ../src/cli/service/resources/resource_catalog.cc + + # Integration Tests + integration/asar_integration_test.cc + integration/asar_rom_test.cc + integration/dungeon_editor_test.cc + integration/dungeon_editor_test.h + integration/dungeon_editor_v2_test.cc + integration/dungeon_editor_v2_test.h + integration/editor/tile16_editor_test.cc + integration/editor/editor_integration_test.cc + integration/editor/editor_integration_test.h + integration/ai/ai_gui_controller_test.cc + integration/ai/test_ai_tile_placement.cc + integration/ai/test_gemini_vision.cc + + # E2E Tests + e2e/rom_dependent/e2e_rom_test.cc + e2e/zscustomoverworld/zscustomoverworld_upgrade_test.cc + + # Integration Tests (Zelda3) + integration/zelda3/overworld_integration_test.cc + integration/zelda3/dungeon_editor_system_integration_test.cc + integration/zelda3/dungeon_object_renderer_integration_test.cc + integration/zelda3/room_integration_test.cc + integration/zelda3/dungeon_object_rendering_tests.cc + integration/zelda3/dungeon_room_test.cc + integration/zelda3/sprite_position_test.cc + integration/zelda3/message_test.cc + ) + + # Sources only for full development builds + set(YAZE_TEST_DEV_SOURCES + test_utils.cc + + # E2E Tests (included in development builds) + e2e/canvas_selection_test.cc + e2e/framework_smoke_test.cc + e2e/dungeon_editor_smoke_test.cc + e2e/dungeon_editor_tests.cc + + # Benchmarks + benchmarks/gfx_optimization_benchmarks.cc + ) + if(YAZE_MINIMAL_BUILD) - # CI/Minimal build: use simplified test executable + # CI/Minimal build: use simplified test executable and base sources add_executable( yaze_test yaze_test_ci.cc - # Emulator unit tests - unit/emu/apu_dsp_test.cc - unit/emu/spc700_reset_test.cc - test_editor.cc - test_editor.h - testing.h - test_utils.h - - # Unit Tests - unit/core/asar_wrapper_test.cc - unit/core/hex_test.cc - unit/cli/resource_catalog_test.cc - unit/rom/rom_test.cc - unit/gfx/snes_tile_test.cc - unit/gfx/compression_test.cc - unit/gfx/snes_palette_test.cc - unit/gui/tile_selector_widget_test.cc - unit/gui/canvas_automation_api_test.cc - unit/zelda3/message_test.cc - unit/zelda3/overworld_test.cc - unit/zelda3/object_parser_test.cc - unit/zelda3/object_parser_structs_test.cc - unit/zelda3/sprite_builder_test.cc - unit/zelda3/sprite_position_test.cc - unit/zelda3/test_dungeon_objects.cc - unit/zelda3/dungeon_component_unit_test.cc - unit/zelda3/dungeon/room_object_encoding_test.cc - zelda3/dungeon/room_manipulation_test.cc - - # CLI Services (for catalog serialization tests) - ../src/cli/service/resources/resource_catalog.cc - - # Integration Tests - integration/asar_integration_test.cc - integration/asar_rom_test.cc - integration/dungeon_editor_test.cc - integration/dungeon_editor_test.h - integration/dungeon_editor_v2_test.cc - integration/dungeon_editor_v2_test.h - integration/editor/tile16_editor_test.cc - integration/editor/editor_integration_test.cc - integration/editor/editor_integration_test.h - - # E2E Tests (excluded in CI builds) - e2e/rom_dependent/e2e_rom_test.cc - e2e/zscustomoverworld/zscustomoverworld_upgrade_test.cc - - # Deprecated Tests (formerly legacy) - deprecated/comprehensive_integration_test.cc - deprecated/dungeon_integration_test.cc - - # Integration Tests (Zelda3) - integration/zelda3/overworld_integration_test.cc - integration/zelda3/dungeon_editor_system_integration_test.cc - integration/zelda3/dungeon_object_renderer_integration_test.cc - integration/zelda3/room_integration_test.cc - - # Mock/Unit Tests for Zelda3 - unit/zelda3/dungeon_object_renderer_mock_test.cc - unit/zelda3/dungeon_object_rendering_tests.cc - unit/zelda3/dungeon_room_test.cc + ${YAZE_TEST_BASE_SOURCES} ) else() - # Development build: use full-featured test executable + # Development build: use full-featured test executable and all sources add_executable( yaze_test yaze_test.cc - # Emulator unit tests - unit/emu/apu_dsp_test.cc - unit/emu/spc700_reset_test.cc - test_editor.cc - test_editor.h - testing.h - test_utils.h - test_utils.cc - - # Unit Tests - unit/core/asar_wrapper_test.cc - unit/core/hex_test.cc - unit/cli/resource_catalog_test.cc - unit/rom/rom_test.cc - unit/gfx/snes_tile_test.cc - unit/gfx/compression_test.cc - unit/gfx/snes_palette_test.cc - unit/gui/tile_selector_widget_test.cc - unit/gui/canvas_automation_api_test.cc - unit/zelda3/message_test.cc - unit/zelda3/overworld_test.cc - unit/zelda3/object_parser_test.cc - unit/zelda3/object_parser_structs_test.cc - unit/zelda3/sprite_builder_test.cc - unit/zelda3/sprite_position_test.cc - unit/zelda3/test_dungeon_objects.cc - unit/zelda3/dungeon_component_unit_test.cc - unit/zelda3/dungeon/room_object_encoding_test.cc - zelda3/dungeon/room_manipulation_test.cc - - # CLI Services (for catalog serialization tests) - ../src/cli/service/resources/resource_catalog.cc - - # Integration Tests - integration/asar_integration_test.cc - integration/asar_rom_test.cc - integration/dungeon_editor_test.cc - integration/dungeon_editor_test.h - integration/dungeon_editor_v2_test.cc - integration/dungeon_editor_v2_test.h - integration/editor/tile16_editor_test.cc - integration/editor/editor_integration_test.cc - integration/editor/editor_integration_test.h - - # E2E Tests (included in development builds) - e2e/canvas_selection_test.cc - e2e/framework_smoke_test.cc - e2e/dungeon_editor_smoke_test.cc - e2e/rom_dependent/e2e_rom_test.cc - e2e/zscustomoverworld/zscustomoverworld_upgrade_test.cc - - # Deprecated Tests (formerly legacy) - deprecated/comprehensive_integration_test.cc - deprecated/dungeon_integration_test.cc - - # Integration Tests (Zelda3) - integration/zelda3/overworld_integration_test.cc - integration/zelda3/dungeon_editor_system_integration_test.cc - integration/zelda3/dungeon_object_renderer_integration_test.cc - integration/zelda3/room_integration_test.cc - - # Mock/Unit Tests for Zelda3 - unit/zelda3/dungeon_object_renderer_mock_test.cc - unit/zelda3/dungeon_object_rendering_tests.cc - unit/zelda3/dungeon_room_test.cc - - # Benchmarks - benchmarks/gfx_optimization_benchmarks.cc + ${YAZE_TEST_BASE_SOURCES} + ${YAZE_TEST_DEV_SOURCES} ) endif() diff --git a/test/benchmarks/gfx_optimization_benchmarks.cc b/test/benchmarks/gfx_optimization_benchmarks.cc index 9495dfa0..9a31f192 100644 --- a/test/benchmarks/gfx_optimization_benchmarks.cc +++ b/test/benchmarks/gfx_optimization_benchmarks.cc @@ -177,7 +177,8 @@ TEST_F(GraphicsOptimizationBenchmarks, BatchTextureUpdatePerformance) { auto start = std::chrono::high_resolution_clock::now(); for (auto& bitmap : bitmaps) { - bitmap.UpdateTexture(nullptr); // Simulate renderer + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &bitmap); } auto end = std::chrono::high_resolution_clock::now(); @@ -187,9 +188,10 @@ TEST_F(GraphicsOptimizationBenchmarks, BatchTextureUpdatePerformance) { start = std::chrono::high_resolution_clock::now(); for (auto& bitmap : bitmaps) { - bitmap.QueueTextureUpdate(nullptr); // Queue for batch processing + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::UPDATE, &bitmap); } - arena.ProcessBatchTextureUpdates(); // Process all at once + gfx::Arena::Get().ProcessTextureQueue(nullptr); // Process all at once end = std::chrono::high_resolution_clock::now(); auto batch_duration = std::chrono::duration_cast(end - start); @@ -413,9 +415,9 @@ TEST_F(GraphicsOptimizationBenchmarks, OverallPerformanceIntegration) { start = std::chrono::high_resolution_clock::now(); for (auto& sheet : graphics_sheets) { - sheet.QueueTextureUpdate(nullptr); + arena.QueueTextureCommand(gfx::Arena::TextureCommandType::UPDATE, &sheet); } - arena.ProcessBatchTextureUpdates(); + arena.ProcessTextureQueue(nullptr); end = std::chrono::high_resolution_clock::now(); auto batch_duration = std::chrono::duration_cast(end - start); diff --git a/test/deprecated/comprehensive_integration_test.cc b/test/deprecated/comprehensive_integration_test.cc deleted file mode 100644 index b87998cf..00000000 --- a/test/deprecated/comprehensive_integration_test.cc +++ /dev/null @@ -1,374 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "app/rom.h" -#include "app/zelda3/overworld/overworld.h" -#include "app/zelda3/overworld/overworld_map.h" - -namespace yaze { -namespace zelda3 { - -class ComprehensiveIntegrationTest : public ::testing::Test { - protected: - void SetUp() override { - // Skip tests on Linux for automated github builds -#if defined(__linux__) - GTEST_SKIP(); -#endif - - vanilla_rom_path_ = "zelda3.sfc"; - v3_rom_path_ = "zelda3_v3_test.sfc"; - - // Create v3 patched ROM for testing - CreateV3PatchedROM(); - - // Load vanilla ROM - vanilla_rom_ = std::make_unique(); - ASSERT_TRUE(vanilla_rom_->LoadFromFile(vanilla_rom_path_).ok()); - - // TODO: Load graphics data when gfx system is available - // ASSERT_TRUE(gfx::LoadAllGraphicsData(*vanilla_rom_, true).ok()); - - // Initialize vanilla overworld - vanilla_overworld_ = std::make_unique(vanilla_rom_.get()); - ASSERT_TRUE(vanilla_overworld_->Load(vanilla_rom_.get()).ok()); - - // Load v3 ROM - v3_rom_ = std::make_unique(); - ASSERT_TRUE(v3_rom_->LoadFromFile(v3_rom_path_).ok()); - - // TODO: Load graphics data when gfx system is available - // ASSERT_TRUE(gfx::LoadAllGraphicsData(*v3_rom_, true).ok()); - - // Initialize v3 overworld - v3_overworld_ = std::make_unique(v3_rom_.get()); - ASSERT_TRUE(v3_overworld_->Load(v3_rom_.get()).ok()); - } - - void TearDown() override { - v3_overworld_.reset(); - vanilla_overworld_.reset(); - v3_rom_.reset(); - vanilla_rom_.reset(); - // TODO: Destroy graphics data when gfx system is available - // gfx::DestroyAllGraphicsData(); - - // Clean up test files - if (std::filesystem::exists(v3_rom_path_)) { - std::filesystem::remove(v3_rom_path_); - } - } - - void CreateV3PatchedROM() { - // Copy vanilla ROM and apply v3 patch - std::ifstream src(vanilla_rom_path_, std::ios::binary); - std::ofstream dst(v3_rom_path_, std::ios::binary); - dst << src.rdbuf(); - src.close(); - dst.close(); - - // Load the copied ROM - Rom rom; - ASSERT_TRUE(rom.LoadFromFile(v3_rom_path_).ok()); - - // Apply v3 patch - ApplyV3Patch(rom); - - // Save the patched ROM - ASSERT_TRUE( - rom.SaveToFile(Rom::SaveSettings{.filename = v3_rom_path_}).ok()); - } - - void ApplyV3Patch(Rom& rom) { - // Set ASM version to v3 - ASSERT_TRUE(rom.WriteByte(OverworldCustomASMHasBeenApplied, 0x03).ok()); - - // Enable v3 features - ASSERT_TRUE(rom.WriteByte(OverworldCustomAreaSpecificBGEnabled, 0x01).ok()); - ASSERT_TRUE(rom.WriteByte(OverworldCustomSubscreenOverlayEnabled, 0x01).ok()); - ASSERT_TRUE(rom.WriteByte(OverworldCustomAnimatedGFXEnabled, 0x01).ok()); - ASSERT_TRUE(rom.WriteByte(OverworldCustomTileGFXGroupEnabled, 0x01).ok()); - ASSERT_TRUE(rom.WriteByte(OverworldCustomMosaicEnabled, 0x01).ok()); - ASSERT_TRUE(rom.WriteByte(OverworldCustomMainPaletteEnabled, 0x01).ok()); - - // Apply v3 settings to first 10 maps for testing - for (int i = 0; i < 10; i++) { - // Set area sizes (mix of different sizes) - AreaSizeEnum area_size = static_cast(i % 4); - ASSERT_TRUE(rom.WriteByte(kOverworldScreenSize + i, static_cast(area_size)).ok()); - - // Set main palettes - ASSERT_TRUE(rom.WriteByte(OverworldCustomMainPaletteArray + i, i % 8).ok()); - - // Set area-specific background colors - uint16_t bg_color = 0x0000 + (i * 0x1000); - ASSERT_TRUE(rom.WriteByte(OverworldCustomAreaSpecificBGPalette + (i * 2), - bg_color & 0xFF).ok()); - ASSERT_TRUE(rom.WriteByte(OverworldCustomAreaSpecificBGPalette + (i * 2) + 1, - (bg_color >> 8) & 0xFF).ok()); - - // Set subscreen overlays - uint16_t overlay = 0x0090 + i; - ASSERT_TRUE(rom.WriteByte(OverworldCustomSubscreenOverlayArray + (i * 2), - overlay & 0xFF).ok()); - ASSERT_TRUE(rom.WriteByte(OverworldCustomSubscreenOverlayArray + (i * 2) + 1, - (overlay >> 8) & 0xFF).ok()); - - // Set animated GFX - ASSERT_TRUE(rom.WriteByte(OverworldCustomAnimatedGFXArray + i, 0x50 + i).ok()); - - // Set custom tile GFX groups (8 bytes per map) - for (int j = 0; j < 8; j++) { - ASSERT_TRUE(rom.WriteByte(OverworldCustomTileGFXGroupArray + (i * 8) + j, - 0x20 + j + i).ok()); - } - - // Set mosaic settings - ASSERT_TRUE(rom.WriteByte(OverworldCustomMosaicArray + i, i % 16).ok()); - - // Set expanded message IDs - uint16_t message_id = 0x1000 + i; - ASSERT_TRUE(rom.WriteByte(kOverworldMessagesExpanded + (i * 2), message_id & 0xFF).ok()); - ASSERT_TRUE(rom.WriteByte(kOverworldMessagesExpanded + (i * 2) + 1, - (message_id >> 8) & 0xFF).ok()); - } - } - - std::string vanilla_rom_path_; - std::string v3_rom_path_; - std::unique_ptr vanilla_rom_; - std::unique_ptr v3_rom_; - std::unique_ptr vanilla_overworld_; - std::unique_ptr v3_overworld_; -}; - -// Test vanilla ROM behavior -TEST_F(ComprehensiveIntegrationTest, VanillaROMDetection) { - uint8_t vanilla_asm_version = - (*vanilla_rom_)[OverworldCustomASMHasBeenApplied]; - EXPECT_EQ(vanilla_asm_version, 0xFF); // 0xFF means vanilla ROM -} - -TEST_F(ComprehensiveIntegrationTest, VanillaROMMapProperties) { - // Test a few specific maps from vanilla ROM - const OverworldMap* map0 = vanilla_overworld_->overworld_map(0); - const OverworldMap* map3 = vanilla_overworld_->overworld_map(3); - const OverworldMap* map64 = vanilla_overworld_->overworld_map(64); - - ASSERT_NE(map0, nullptr); - ASSERT_NE(map3, nullptr); - ASSERT_NE(map64, nullptr); - - // Verify basic properties are loaded - EXPECT_GE(map0->area_graphics(), 0); - EXPECT_GE(map0->area_palette(), 0); - EXPECT_GE(map0->message_id(), 0); - EXPECT_GE(map3->area_graphics(), 0); - EXPECT_GE(map3->area_palette(), 0); - EXPECT_GE(map64->area_graphics(), 0); - EXPECT_GE(map64->area_palette(), 0); - - // Verify area sizes are reasonable - EXPECT_TRUE(map0->area_size() == AreaSizeEnum::SmallArea || - map0->area_size() == AreaSizeEnum::LargeArea); - EXPECT_TRUE(map3->area_size() == AreaSizeEnum::SmallArea || - map3->area_size() == AreaSizeEnum::LargeArea); - EXPECT_TRUE(map64->area_size() == AreaSizeEnum::SmallArea || - map64->area_size() == AreaSizeEnum::LargeArea); -} - -// Test v3 ROM behavior -TEST_F(ComprehensiveIntegrationTest, V3ROMDetection) { - uint8_t v3_asm_version = (*v3_rom_)[OverworldCustomASMHasBeenApplied]; - EXPECT_EQ(v3_asm_version, 0x03); // 0x03 means v3 ROM -} - -TEST_F(ComprehensiveIntegrationTest, V3ROMFeatureFlags) { - // Test that v3 features are enabled - EXPECT_EQ((*v3_rom_)[OverworldCustomAreaSpecificBGEnabled], 0x01); - EXPECT_EQ((*v3_rom_)[OverworldCustomSubscreenOverlayEnabled], 0x01); - EXPECT_EQ((*v3_rom_)[OverworldCustomAnimatedGFXEnabled], 0x01); - EXPECT_EQ((*v3_rom_)[OverworldCustomTileGFXGroupEnabled], 0x01); - EXPECT_EQ((*v3_rom_)[OverworldCustomMosaicEnabled], 0x01); - EXPECT_EQ((*v3_rom_)[OverworldCustomMainPaletteEnabled], 0x01); -} - -TEST_F(ComprehensiveIntegrationTest, V3ROMAreaSizes) { - // Test that v3 area sizes are loaded correctly - for (int i = 0; i < 10; i++) { - const OverworldMap* map = v3_overworld_->overworld_map(i); - ASSERT_NE(map, nullptr); - - AreaSizeEnum expected_size = static_cast(i % 4); - EXPECT_EQ(map->area_size(), expected_size); - } -} - -TEST_F(ComprehensiveIntegrationTest, V3ROMMainPalettes) { - // Test that v3 main palettes are loaded correctly - for (int i = 0; i < 10; i++) { - const OverworldMap* map = v3_overworld_->overworld_map(i); - ASSERT_NE(map, nullptr); - - uint8_t expected_palette = i % 8; - EXPECT_EQ(map->main_palette(), expected_palette); - } -} - -TEST_F(ComprehensiveIntegrationTest, V3ROMAreaSpecificBackgroundColors) { - // Test that v3 area-specific background colors are loaded correctly - for (int i = 0; i < 10; i++) { - const OverworldMap* map = v3_overworld_->overworld_map(i); - ASSERT_NE(map, nullptr); - - uint16_t expected_color = 0x0000 + (i * 0x1000); - EXPECT_EQ(map->area_specific_bg_color(), expected_color); - } -} - -TEST_F(ComprehensiveIntegrationTest, V3ROMSubscreenOverlays) { - // Test that v3 subscreen overlays are loaded correctly - for (int i = 0; i < 10; i++) { - const OverworldMap* map = v3_overworld_->overworld_map(i); - ASSERT_NE(map, nullptr); - - uint16_t expected_overlay = 0x0090 + i; - EXPECT_EQ(map->subscreen_overlay(), expected_overlay); - } -} - -TEST_F(ComprehensiveIntegrationTest, V3ROMAnimatedGFX) { - // Test that v3 animated GFX are loaded correctly - for (int i = 0; i < 10; i++) { - const OverworldMap* map = v3_overworld_->overworld_map(i); - ASSERT_NE(map, nullptr); - - uint8_t expected_gfx = 0x50 + i; - EXPECT_EQ(map->animated_gfx(), expected_gfx); - } -} - -TEST_F(ComprehensiveIntegrationTest, V3ROMCustomTileGFXGroups) { - // Test that v3 custom tile GFX groups are loaded correctly - for (int i = 0; i < 10; i++) { - const OverworldMap* map = v3_overworld_->overworld_map(i); - ASSERT_NE(map, nullptr); - - for (int j = 0; j < 8; j++) { - uint8_t expected_tile = 0x20 + j + i; - EXPECT_EQ(map->custom_tileset(j), expected_tile); - } - } -} - -TEST_F(ComprehensiveIntegrationTest, V3ROMExpandedMessageIds) { - // Test that v3 expanded message IDs are loaded correctly - for (int i = 0; i < 10; i++) { - const OverworldMap* map = v3_overworld_->overworld_map(i); - ASSERT_NE(map, nullptr); - - uint16_t expected_message_id = 0x1000 + i; - EXPECT_EQ(map->message_id(), expected_message_id); - } -} - -// Test backwards compatibility -TEST_F(ComprehensiveIntegrationTest, BackwardsCompatibility) { - // Test that v3 ROMs still have access to vanilla properties - for (int i = 0; i < 10; i++) { - const OverworldMap* vanilla_map = vanilla_overworld_->overworld_map(i); - const OverworldMap* v3_map = v3_overworld_->overworld_map(i); - - ASSERT_NE(vanilla_map, nullptr); - ASSERT_NE(v3_map, nullptr); - - // Basic properties should still be accessible - EXPECT_GE(v3_map->area_graphics(), 0); - EXPECT_GE(v3_map->area_palette(), 0); - EXPECT_GE(v3_map->message_id(), 0); - } -} - -// Test save/load functionality -TEST_F(ComprehensiveIntegrationTest, SaveAndReloadV3ROM) { - // Modify some properties - v3_overworld_->mutable_overworld_map(0)->set_main_palette(0x07); - v3_overworld_->mutable_overworld_map(1)->set_area_specific_bg_color(0x7FFF); - v3_overworld_->mutable_overworld_map(2)->set_subscreen_overlay(0x1234); - - // Save the ROM - ASSERT_TRUE(v3_overworld_->Save(v3_rom_.get()).ok()); - - // Reload the ROM - Rom reloaded_rom; - ASSERT_TRUE(reloaded_rom.LoadFromFile(v3_rom_path_).ok()); - - Overworld reloaded_overworld(&reloaded_rom); - ASSERT_TRUE(reloaded_overworld.Load(&reloaded_rom).ok()); - - // Verify the changes were saved - EXPECT_EQ(reloaded_overworld.overworld_map(0)->main_palette(), 0x07); - EXPECT_EQ(reloaded_overworld.overworld_map(1)->area_specific_bg_color(), - 0x7FFF); - EXPECT_EQ(reloaded_overworld.overworld_map(2)->subscreen_overlay(), 0x1234); -} - -// Performance test -TEST_F(ComprehensiveIntegrationTest, PerformanceTest) { - const int kNumMaps = 160; - - auto start_time = std::chrono::high_resolution_clock::now(); - - // Test vanilla ROM performance - for (int i = 0; i < kNumMaps; i++) { - const OverworldMap* map = vanilla_overworld_->overworld_map(i); - if (map) { - map->area_graphics(); - map->area_palette(); - map->message_id(); - map->area_size(); - } - } - - // Test v3 ROM performance - for (int i = 0; i < kNumMaps; i++) { - const OverworldMap* map = v3_overworld_->overworld_map(i); - if (map) { - map->area_graphics(); - map->area_palette(); - map->message_id(); - map->area_size(); - map->main_palette(); - map->area_specific_bg_color(); - map->subscreen_overlay(); - map->animated_gfx(); - } - } - - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast( - end_time - start_time); - - // Should complete in reasonable time (less than 2 seconds for 320 map - // operations) - EXPECT_LT(duration.count(), 2000); -} - -// Test dungeon integration (if applicable) -TEST_F(ComprehensiveIntegrationTest, DungeonIntegration) { - // This test ensures that overworld changes don't break dungeon functionality - // For now, just verify that the ROMs can be loaded without errors - EXPECT_TRUE(vanilla_overworld_->is_loaded()); - EXPECT_TRUE(v3_overworld_->is_loaded()); - - // Verify that we have the expected number of maps - EXPECT_EQ(vanilla_overworld_->overworld_maps().size(), kNumOverworldMaps); - EXPECT_EQ(v3_overworld_->overworld_maps().size(), kNumOverworldMaps); -} - -} // namespace zelda3 -} // namespace yaze diff --git a/test/deprecated/dungeon_integration_test.cc b/test/deprecated/dungeon_integration_test.cc deleted file mode 100644 index 9e722cde..00000000 --- a/test/deprecated/dungeon_integration_test.cc +++ /dev/null @@ -1,208 +0,0 @@ -#include -#include -#include -#include - -#include "app/rom.h" -#include "app/zelda3/overworld/overworld.h" -#include "app/zelda3/overworld/overworld_map.h" - -namespace yaze { -namespace zelda3 { - -class DungeonIntegrationTest : public ::testing::Test { -protected: - void SetUp() override { - // Skip tests on Linux for automated github builds -#if defined(__linux__) - GTEST_SKIP(); -#endif - - rom_path_ = "zelda3.sfc"; - - // Load ROM - rom_ = std::make_unique(); - ASSERT_TRUE(rom_->LoadFromFile(rom_path_).ok()); - - // TODO: Load graphics data when gfx system is available - // ASSERT_TRUE(gfx::LoadAllGraphicsData(*rom_, true).ok()); - - // Initialize overworld - overworld_ = std::make_unique(rom_.get()); - ASSERT_TRUE(overworld_->Load(rom_.get()).ok()); - } - - void TearDown() override { - overworld_.reset(); - rom_.reset(); - // TODO: Destroy graphics data when gfx system is available - // gfx::DestroyAllGraphicsData(); - } - - std::string rom_path_; - std::unique_ptr rom_; - std::unique_ptr overworld_; -}; - -// Test dungeon room loading -TEST_F(DungeonIntegrationTest, DungeonRoomLoading) { - // TODO: Implement dungeon room loading tests when Room class is available - // Test loading a few dungeon rooms - const int kNumTestRooms = 10; - - for (int i = 0; i < kNumTestRooms; i++) { - // TODO: Create Room instance and test basic properties - // Room room(i, rom_.get()); - // EXPECT_EQ(room.index(), i); - // EXPECT_GE(room.width(), 0); - // EXPECT_GE(room.height(), 0); - // auto status = room.Build(); - // EXPECT_TRUE(status.ok()) << "Failed to build room " << i << ": " << status.message(); - } -} - -// Test dungeon object parsing -TEST_F(DungeonIntegrationTest, DungeonObjectParsing) { - // TODO: Implement dungeon object parsing tests when ObjectParser is available - // Test object parsing for a few rooms - const int kNumTestRooms = 5; - - for (int i = 0; i < kNumTestRooms; i++) { - // TODO: Create Room and ObjectParser instances - // Room room(i, rom_.get()); - // ASSERT_TRUE(room.Build().ok()); - // ObjectParser parser(room); - // auto objects = parser.ParseObjects(); - // EXPECT_TRUE(objects.ok()) << "Failed to parse objects for room " << i << ": " << objects.status().message(); - // if (objects.ok()) { - // for (const auto& obj : objects.value()) { - // EXPECT_GE(obj.x(), 0); - // EXPECT_GE(obj.y(), 0); - // EXPECT_GE(obj.type(), 0); - // } - // } - } -} - -// Test dungeon object rendering -TEST_F(DungeonIntegrationTest, DungeonObjectRendering) { - // TODO: Implement dungeon object rendering tests when ObjectRenderer is available - // Test object rendering for a few rooms - const int kNumTestRooms = 3; - - for (int i = 0; i < kNumTestRooms; i++) { - // TODO: Create Room, ObjectParser, and ObjectRenderer instances - // Room room(i, rom_.get()); - // ASSERT_TRUE(room.Build().ok()); - // ObjectParser parser(room); - // auto objects = parser.ParseObjects(); - // ASSERT_TRUE(objects.ok()); - // ObjectRenderer renderer(room); - // auto status = renderer.RenderObjects(objects.value()); - // EXPECT_TRUE(status.ok()) << "Failed to render objects for room " << i << ": " << status.message(); - } -} - -// Test dungeon integration with overworld -TEST_F(DungeonIntegrationTest, DungeonOverworldIntegration) { - // Test that dungeon changes don't affect overworld functionality - EXPECT_TRUE(overworld_->is_loaded()); - EXPECT_EQ(overworld_->overworld_maps().size(), kNumOverworldMaps); - - // Test that we can access overworld maps after dungeon operations - const OverworldMap* map0 = overworld_->overworld_map(0); - ASSERT_NE(map0, nullptr); - - // Verify basic overworld properties still work - EXPECT_GE(map0->area_graphics(), 0); - EXPECT_GE(map0->area_palette(), 0); - EXPECT_GE(map0->message_id(), 0); -} - -// Test ROM integrity after dungeon operations -TEST_F(DungeonIntegrationTest, ROMIntegrity) { - // Test that ROM remains intact after dungeon operations - // std::vector original_data = rom_->data(); - - // // Perform various dungeon operations - // for (int i = 0; i < 5; i++) { - // Room room(i, rom_.get()); - // room.Build(); - - // ObjectParser parser(room); - // parser.ParseObjects(); - // } - - // // Verify ROM data hasn't changed - // std::vector current_data = rom_->data(); - // EXPECT_EQ(original_data.size(), current_data.size()); - - // // Check that critical ROM areas haven't been corrupted - // EXPECT_EQ(rom_->data()[0x7FC0], original_data[0x7FC0]); // ROM header - // EXPECT_EQ(rom_->data()[0x7FC1], original_data[0x7FC1]); - // EXPECT_EQ(rom_->data()[0x7FC2], original_data[0x7FC2]); -} - -// Performance test for dungeon operations -TEST_F(DungeonIntegrationTest, DungeonPerformanceTest) { - // TODO: Implement dungeon performance tests when dungeon classes are available - const int kNumRooms = 50; - - auto start_time = std::chrono::high_resolution_clock::now(); - - for (int i = 0; i < kNumRooms; i++) { - // TODO: Create Room and ObjectParser instances for performance testing - // Room room(i, rom_.get()); - // room.Build(); - // ObjectParser parser(room); - // parser.ParseObjects(); - } - - auto end_time = std::chrono::high_resolution_clock::now(); - auto duration = std::chrono::duration_cast(end_time - start_time); - - // Should complete in reasonable time (less than 5 seconds for 50 rooms) - EXPECT_LT(duration.count(), 5000); -} - -// Test dungeon save/load functionality -TEST_F(DungeonIntegrationTest, DungeonSaveLoad) { - // TODO: Implement dungeon save/load tests when dungeon classes are available - // Create a test room - // Room room(0, rom_.get()); - // ASSERT_TRUE(room.Build().ok()); - - // Parse objects - // ObjectParser parser(room); - // auto objects = parser.ParseObjects(); - // ASSERT_TRUE(objects.ok()); - - // Modify some objects (if any exist) - // if (!objects.value().empty()) { - // // This would involve modifying object properties and saving - // // For now, just verify the basic save/load mechanism works - // EXPECT_TRUE(rom_->SaveToFile("test_dungeon.sfc").ok()); - // - // // Clean up test file - // if (std::filesystem::exists("test_dungeon.sfc")) { - // std::filesystem::remove("test_dungeon.sfc"); - // } - // } -} - -// Test dungeon error handling -TEST_F(DungeonIntegrationTest, DungeonErrorHandling) { - // TODO: Implement dungeon error handling tests when Room class is available - // Test with invalid room indices - // Room invalid_room(-1, rom_.get()); - // auto status = invalid_room.Build(); - // EXPECT_FALSE(status.ok()); // Should fail for invalid room - - // Test with very large room index - // Room large_room(1000, rom_.get()); - // status = large_room.Build(); - // EXPECT_FALSE(status.ok()); // Should fail for non-existent room -} - -} // namespace zelda3 -} // namespace yaze diff --git a/test/imgui/dungeon_editor_tests.cc b/test/e2e/dungeon_editor_tests.cc similarity index 99% rename from test/imgui/dungeon_editor_tests.cc rename to test/e2e/dungeon_editor_tests.cc index c8d8158d..ee5f2d37 100644 --- a/test/imgui/dungeon_editor_tests.cc +++ b/test/e2e/dungeon_editor_tests.cc @@ -7,7 +7,7 @@ #endif #include "app/editor/dungeon/dungeon_editor.h" -#include "app/gui/widget_id_registry.h" +#include "app/gui/widgets/widget_id_registry.h" #include "app/rom.h" namespace yaze { diff --git a/test/integration/ai_gui_controller_test.cc b/test/integration/ai/ai_gui_controller_test.cc similarity index 100% rename from test/integration/ai_gui_controller_test.cc rename to test/integration/ai/ai_gui_controller_test.cc diff --git a/test/integration/test_ai_tile_placement.cc b/test/integration/ai/test_ai_tile_placement.cc similarity index 98% rename from test/integration/test_ai_tile_placement.cc rename to test/integration/ai/test_ai_tile_placement.cc index 693c84c7..768fbf56 100644 --- a/test/integration/test_ai_tile_placement.cc +++ b/test/integration/ai/test_ai_tile_placement.cc @@ -128,8 +128,8 @@ TEST_F(AITilePlacementTest, DISABLED_FullAIControlLoop) { gemini_config.api_key = api_key; cli::GeminiAIService gemini_service(gemini_config); - cli::gui::GuiAutomationClient gui_client; - auto connect_status = gui_client.Connect("localhost", 50051); + cli::GuiAutomationClient gui_client("localhost:50051"); + auto connect_status = gui_client.Connect(); if (!connect_status.ok()) { GTEST_SKIP() << "GUI test harness not available: " << connect_status.message(); diff --git a/test/multimodal/test_gemini_vision.cc b/test/integration/ai/test_gemini_vision.cc similarity index 94% rename from test/multimodal/test_gemini_vision.cc rename to test/integration/ai/test_gemini_vision.cc index d6549ad2..8879ad56 100644 --- a/test/multimodal/test_gemini_vision.cc +++ b/test/integration/ai/test_gemini_vision.cc @@ -163,7 +163,7 @@ TEST_F(GeminiVisionTest, ScreenshotCaptureIntegration) { // Analyze the captured screenshot auto response = service.GenerateMultimodalResponse( - screenshot_result->file_path.string(), + screenshot_result->file_path, "What UI elements are visible in this screenshot? List them." ); @@ -240,12 +240,4 @@ TEST_F(GeminiVisionTest, RateLimitHandling) { } // namespace test } // namespace yaze -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - - std::cout << "\n=== Gemini Multimodal Vision Tests ===" << std::endl; - std::cout << "These tests require GEMINI_API_KEY environment variable." << std::endl; - std::cout << "Tests will be skipped if API key is not available.\n" << std::endl; - - return RUN_ALL_TESTS(); -} +// Note: main() is provided by yaze_test.cc for the unified test runner diff --git a/test/integration/editor/editor_integration_test.cc b/test/integration/editor/editor_integration_test.cc index 6ef82339..9dcbbdc5 100644 --- a/test/integration/editor/editor_integration_test.cc +++ b/test/integration/editor/editor_integration_test.cc @@ -38,7 +38,9 @@ EditorIntegrationTest::~EditorIntegrationTest() { } absl::Status EditorIntegrationTest::Initialize() { - RETURN_IF_ERROR(core::CreateWindow(window_, SDL_WINDOW_RESIZABLE)); + // Create renderer for test + test_renderer_ = std::make_unique(); + RETURN_IF_ERROR(core::CreateWindow(window_, test_renderer_.get(), SDL_WINDOW_RESIZABLE)); IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -55,9 +57,9 @@ absl::Status EditorIntegrationTest::Initialize() { io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Initialize ImGui for SDL - ImGui_ImplSDL2_InitForSDLRenderer( - controller_.window(), yaze::core::Renderer::Get().renderer()); - ImGui_ImplSDLRenderer2_Init(yaze::core::Renderer::Get().renderer()); + SDL_Renderer* sdl_renderer = static_cast(test_renderer_->GetBackendRenderer()); + ImGui_ImplSDL2_InitForSDLRenderer(controller_.window(), sdl_renderer); + ImGui_ImplSDLRenderer2_Init(sdl_renderer); #ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE // Register tests diff --git a/test/integration/editor/editor_integration_test.h b/test/integration/editor/editor_integration_test.h index 68979107..88e1fc3d 100644 --- a/test/integration/editor/editor_integration_test.h +++ b/test/integration/editor/editor_integration_test.h @@ -8,6 +8,7 @@ #include "app/rom.h" #include "app/core/controller.h" #include "app/core/window.h" +#include "app/gfx/backend/sdl2_renderer.h" #ifdef YAZE_ENABLE_IMGUI_TEST_ENGINE #include "imgui_test_engine/imgui_te_context.h" @@ -81,6 +82,7 @@ class EditorIntegrationTest { #endif std::unique_ptr test_rom_; core::Window window_; + std::unique_ptr test_renderer_; }; } // namespace test diff --git a/test/integration/editor/tile16_editor_test.cc b/test/integration/editor/tile16_editor_test.cc index 2ca71b12..f2a711fc 100644 --- a/test/integration/editor/tile16_editor_test.cc +++ b/test/integration/editor/tile16_editor_test.cc @@ -6,6 +6,8 @@ #include #include "app/rom.h" +#include "app/gfx/arena.h" +#include "app/gfx/backend/sdl2_renderer.h" #include "app/gfx/bitmap.h" #include "app/gfx/tilemap.h" #include "app/zelda3/overworld/overworld.h" @@ -52,20 +54,22 @@ class Tile16EditorIntegrationTest : public ::testing::Test { auto palette = overworld_->current_area_palette(); tile16_blockset_ = std::make_unique( - gfx::CreateTilemap(tile16_data, 0x80, 0x2000, 16, + gfx::CreateTilemap(nullptr, tile16_data, 0x80, 0x2000, 16, zelda3::kNumTile16Individual, palette)); // Create graphics bitmap current_gfx_bmp_ = std::make_unique(); - core::Renderer::Get().CreateAndRenderBitmap(0x80, 512, 0x40, - overworld_->current_graphics(), - *current_gfx_bmp_, palette); + current_gfx_bmp_->Create(0x80, 512, 0x40, overworld_->current_graphics()); + current_gfx_bmp_->SetPalette(palette); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, current_gfx_bmp_.get()); // Create tile16 blockset bitmap tile16_blockset_bmp_ = std::make_unique(); - core::Renderer::Get().CreateAndRenderBitmap(0x80, 0x2000, 0x08, - tile16_data, - *tile16_blockset_bmp_, palette); + tile16_blockset_bmp_->Create(0x80, 0x2000, 0x08, tile16_data); + tile16_blockset_bmp_->SetPalette(palette); + gfx::Arena::Get().QueueTextureCommand( + gfx::Arena::TextureCommandType::CREATE, tile16_blockset_bmp_.get()); // Initialize the tile16 editor editor_ = std::make_unique(rom_.get(), tile16_blockset_.get()); @@ -85,7 +89,9 @@ class Tile16EditorIntegrationTest : public ::testing::Test { protected: static void InitializeTestEnvironment() { - auto window_result = core::CreateWindow(test_window_, SDL_WINDOW_HIDDEN); + // Create renderer for test + test_renderer_ = std::make_unique(); + auto window_result = core::CreateWindow(test_window_, test_renderer_.get(), SDL_WINDOW_HIDDEN); if (window_result.ok()) { window_initialized_ = true; } else { @@ -97,6 +103,7 @@ protected: static bool window_initialized_; static core::Window test_window_; + static std::unique_ptr test_renderer_; bool rom_loaded_ = false; std::unique_ptr rom_; @@ -111,6 +118,7 @@ protected: // Static member definitions bool Tile16EditorIntegrationTest::window_initialized_ = false; core::Window Tile16EditorIntegrationTest::test_window_; +std::unique_ptr Tile16EditorIntegrationTest::test_renderer_; // Basic validation tests (no ROM required) TEST_F(Tile16EditorIntegrationTest, BasicValidation) { diff --git a/test/unit/zelda3/dungeon_object_rendering_tests.cc b/test/integration/zelda3/dungeon_object_rendering_tests.cc similarity index 100% rename from test/unit/zelda3/dungeon_object_rendering_tests.cc rename to test/integration/zelda3/dungeon_object_rendering_tests.cc diff --git a/test/unit/zelda3/dungeon_room_test.cc b/test/integration/zelda3/dungeon_room_test.cc similarity index 100% rename from test/unit/zelda3/dungeon_room_test.cc rename to test/integration/zelda3/dungeon_room_test.cc diff --git a/test/unit/zelda3/message_test.cc b/test/integration/zelda3/message_test.cc similarity index 100% rename from test/unit/zelda3/message_test.cc rename to test/integration/zelda3/message_test.cc diff --git a/test/unit/zelda3/sprite_position_test.cc b/test/integration/zelda3/sprite_position_test.cc similarity index 100% rename from test/unit/zelda3/sprite_position_test.cc rename to test/integration/zelda3/sprite_position_test.cc diff --git a/test/test_editor.cc b/test/test_editor.cc index 4f0e0bf2..f755fd3f 100644 --- a/test/test_editor.cc +++ b/test/test_editor.cc @@ -4,6 +4,7 @@ #include "app/core/controller.h" #include "app/core/window.h" +#include "app/gfx/backend/sdl2_renderer.h" #include "app/gui/style.h" #include "imgui/backends/imgui_impl_sdl2.h" #include "imgui/backends/imgui_impl_sdlrenderer2.h" @@ -56,7 +57,9 @@ void TestEditor::RegisterTests(ImGuiTestEngine* engine) { int RunIntegrationTest() { yaze::core::Controller controller; yaze::core::Window window; - yaze::core::CreateWindow(window, SDL_WINDOW_RESIZABLE); + // Create renderer for test + auto test_renderer = std::make_unique(); + yaze::core::CreateWindow(window, test_renderer.get(), SDL_WINDOW_RESIZABLE); IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -74,9 +77,9 @@ int RunIntegrationTest() { io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Initialize ImGui for SDL - ImGui_ImplSDL2_InitForSDLRenderer( - controller.window(), yaze::core::Renderer::Get().renderer()); - ImGui_ImplSDLRenderer2_Init(yaze::core::Renderer::Get().renderer()); + SDL_Renderer* sdl_renderer = static_cast(test_renderer->GetBackendRenderer()); + ImGui_ImplSDL2_InitForSDLRenderer(controller.window(), sdl_renderer); + ImGui_ImplSDLRenderer2_Init(sdl_renderer); yaze::test::TestEditor test_editor; #ifdef IMGUI_ENABLE_TEST_ENGINE diff --git a/test/zelda3/dungeon/room_manipulation_test.cc b/test/unit/zelda3/dungeon/room_manipulation_test.cc similarity index 100% rename from test/zelda3/dungeon/room_manipulation_test.cc rename to test/unit/zelda3/dungeon/room_manipulation_test.cc diff --git a/test/unit/zelda3/dungeon/room_object_encoding_test.cc b/test/unit/zelda3/dungeon/room_object_encoding_test.cc index 6168eb81..534c8d32 100644 --- a/test/unit/zelda3/dungeon/room_object_encoding_test.cc +++ b/test/unit/zelda3/dungeon/room_object_encoding_test.cc @@ -231,7 +231,7 @@ TEST(RoomObjectEncodingTest, Type3RealWorldExample) { // Expected: X=10, Y=15, ID=0xF99 (small chest) EXPECT_EQ(decoded.x(), 10); EXPECT_EQ(decoded.y(), 15); - EXPECT_EQ(decoded.id_, 0x99F); + EXPECT_EQ(decoded.id_, 0xF99); } // ============================================================================ diff --git a/test/yaze_test.cc b/test/yaze_test.cc index a030e241..da480e3a 100644 --- a/test/yaze_test.cc +++ b/test/yaze_test.cc @@ -17,6 +17,7 @@ #include "imgui_test_engine/imgui_te_ui.h" #include "app/core/window.h" #include "app/core/controller.h" +#include "app/gfx/backend/sdl2_renderer.h" #include "e2e/canvas_selection_test.h" #include "e2e/framework_smoke_test.h" #include "e2e/dungeon_editor_smoke_test.h" @@ -242,10 +243,11 @@ int main(int argc, char* argv[]) { if (config.enable_ui_tests) { // Create a window yaze::core::Window window; - yaze::core::CreateWindow(window, SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + // Create renderer for test + auto test_renderer = std::make_unique(); + yaze::core::CreateWindow(window, test_renderer.get(), SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); - // Create a renderer - yaze::core::Renderer::Get().CreateRenderer(window.window_.get()); + // Renderer is now owned by test // Setup Dear ImGui context IMGUI_CHECKVERSION(); @@ -266,8 +268,9 @@ int main(int argc, char* argv[]) { } // Setup Platform/Renderer backends - ImGui_ImplSDL2_InitForSDLRenderer(window.window_.get(), yaze::core::Renderer::Get().renderer()); - ImGui_ImplSDLRenderer2_Init(yaze::core::Renderer::Get().renderer()); + SDL_Renderer* sdl_renderer = static_cast(test_renderer->GetBackendRenderer()); + ImGui_ImplSDL2_InitForSDLRenderer(window.window_.get(), sdl_renderer); + ImGui_ImplSDLRenderer2_Init(sdl_renderer); // Setup test engine ImGuiTestEngine* engine = ImGuiTestEngine_CreateContext(); @@ -319,9 +322,9 @@ int main(int argc, char* argv[]) { // End the Dear ImGui frame ImGui::Render(); - yaze::core::Renderer::Get().Clear(); - ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData(), yaze::core::Renderer::Get().renderer()); - yaze::core::Renderer::Get().Present(); + test_renderer->Clear(); + ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData(), sdl_renderer); + test_renderer->Present(); // Update and Render additional Platform Windows if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { diff --git a/test/zelda3/dungeon/room_object_encoding_test.cc b/test/zelda3/dungeon/room_object_encoding_test.cc deleted file mode 100644 index 5fa58a5f..00000000 --- a/test/zelda3/dungeon/room_object_encoding_test.cc +++ /dev/null @@ -1,331 +0,0 @@ -// test/zelda3/dungeon/room_object_encoding_test.cc -// Unit tests for Phase 1, Task 1.1: Object Encoding/Decoding -// -// These tests verify that the object encoding and decoding functions work -// correctly for all three object types (Type1, Type2, Type3) based on -// ZScream's proven implementation. - -#include "app/zelda3/dungeon/room_object.h" - -#include - -namespace yaze { -namespace zelda3 { -namespace { - -// ============================================================================ -// Object Type Detection Tests -// ============================================================================ - -TEST(RoomObjectEncodingTest, DetermineObjectTypeType1) { - // Type1: b1 < 0xFC, b3 < 0xF8 - EXPECT_EQ(RoomObject::DetermineObjectType(0x28, 0x10), 1); - EXPECT_EQ(RoomObject::DetermineObjectType(0x50, 0x42), 1); - EXPECT_EQ(RoomObject::DetermineObjectType(0xFB, 0xF7), 1); -} - -TEST(RoomObjectEncodingTest, DetermineObjectTypeType2) { - // Type2: b1 >= 0xFC, b3 < 0xF8 - EXPECT_EQ(RoomObject::DetermineObjectType(0xFC, 0x42), 2); - EXPECT_EQ(RoomObject::DetermineObjectType(0xFD, 0x25), 2); - EXPECT_EQ(RoomObject::DetermineObjectType(0xFF, 0x00), 2); -} - -TEST(RoomObjectEncodingTest, DetermineObjectTypeType3) { - // Type3: b3 >= 0xF8 - EXPECT_EQ(RoomObject::DetermineObjectType(0x28, 0xF8), 3); - EXPECT_EQ(RoomObject::DetermineObjectType(0x50, 0xF9), 3); - EXPECT_EQ(RoomObject::DetermineObjectType(0xFC, 0xFF), 3); -} - -// ============================================================================ -// Type 1 Object Encoding/Decoding Tests -// ============================================================================ - -TEST(RoomObjectEncodingTest, Type1EncodeDecodeBasic) { - // Type1: xxxxxxss yyyyyyss iiiiiiii - // Example: Object ID 0x42, position (10, 20), size 3, layer 0 - - RoomObject obj(0x42, 10, 20, 3, 0); - - // Encode - auto bytes = obj.EncodeObjectToBytes(); - - // Decode - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); - - // Verify - EXPECT_EQ(decoded.id_, obj.id_); - EXPECT_EQ(decoded.x(), obj.x()); - EXPECT_EQ(decoded.y(), obj.y()); - EXPECT_EQ(decoded.size(), obj.size()); - EXPECT_EQ(decoded.GetLayerValue(), obj.GetLayerValue()); -} - -TEST(RoomObjectEncodingTest, Type1MaxValues) { - // Test maximum valid values for Type1 - // Constraints: - // - ID < 0xF8 (b3 >= 0xF8 triggers Type3 detection) - // - X < 63 OR Size < 12 (b1 >= 0xFC triggers Type2 detection) - // Safe max values: ID=0xF7, X=62, Y=63, Size=15 - RoomObject obj(0xF7, 62, 63, 15, 2); - - auto bytes = obj.EncodeObjectToBytes(); - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 2); - - EXPECT_EQ(decoded.id_, obj.id_); - EXPECT_EQ(decoded.x(), obj.x()); - EXPECT_EQ(decoded.y(), obj.y()); - EXPECT_EQ(decoded.size(), obj.size()); -} - -TEST(RoomObjectEncodingTest, Type1MinValues) { - // Test minimum values for Type1 - RoomObject obj(0x00, 0, 0, 0, 0); - - auto bytes = obj.EncodeObjectToBytes(); - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); - - EXPECT_EQ(decoded.id_, obj.id_); - EXPECT_EQ(decoded.x(), obj.x()); - EXPECT_EQ(decoded.y(), obj.y()); - EXPECT_EQ(decoded.size(), obj.size()); -} - -TEST(RoomObjectEncodingTest, Type1DifferentSizes) { - // Test all valid size values (0-15) - for (int size = 0; size <= 15; size++) { - RoomObject obj(0x30, 15, 20, size, 1); - - auto bytes = obj.EncodeObjectToBytes(); - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1); - - EXPECT_EQ(decoded.size(), size) << "Failed for size " << size; - } -} - -TEST(RoomObjectEncodingTest, Type1RealWorldExample1) { - // Example from actual ROM: Wall object - // Bytes: 0x28 0x50 0x10 - // Expected: X=10, Y=20, Size=0, ID=0x10 - - auto decoded = RoomObject::DecodeObjectFromBytes(0x28, 0x50, 0x10, 0); - - EXPECT_EQ(decoded.x(), 10); - EXPECT_EQ(decoded.y(), 20); - EXPECT_EQ(decoded.size(), 0); - EXPECT_EQ(decoded.id_, 0x10); -} - -TEST(RoomObjectEncodingTest, Type1RealWorldExample2) { - // Example: Ceiling object with size - // Correct bytes for X=10, Y=20, Size=3, ID=0x00: 0x28 0x53 0x00 - - auto decoded = RoomObject::DecodeObjectFromBytes(0x28, 0x53, 0x00, 0); - - EXPECT_EQ(decoded.x(), 10); - EXPECT_EQ(decoded.y(), 20); - EXPECT_EQ(decoded.size(), 3); - EXPECT_EQ(decoded.id_, 0x00); -} - -// ============================================================================ -// Type 2 Object Encoding/Decoding Tests -// ============================================================================ - -TEST(RoomObjectEncodingTest, Type2EncodeDecodeBasic) { - // Type2: 111111xx xxxxyyyy yyiiiiii - // Example: Object ID 0x125, position (15, 30), size ignored, layer 1 - - RoomObject obj(0x125, 15, 30, 0, 1); - - // Encode - auto bytes = obj.EncodeObjectToBytes(); - - // Verify b1 starts with 0xFC - EXPECT_GE(bytes.b1, 0xFC); - - // Decode - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1); - - // Verify - EXPECT_EQ(decoded.id_, obj.id_); - EXPECT_EQ(decoded.x(), obj.x()); - EXPECT_EQ(decoded.y(), obj.y()); - EXPECT_EQ(decoded.GetLayerValue(), obj.GetLayerValue()); -} - -TEST(RoomObjectEncodingTest, Type2MaxValues) { - // Type2 allows larger position range, but has constraints: - // When Y=63 and ID=0x13F, b3 becomes 0xFF >= 0xF8, triggering Type3 detection - // Safe max: X=63, Y=59, ID=0x13F (b3 = ((59&0x03)<<6)|(0x3F) = 0xFF still!) - // Even safer: X=63, Y=63, ID=0x11F (b3 = (0xC0|0x1F) = 0xDF < 0xF8) - RoomObject obj(0x11F, 63, 63, 0, 2); - - auto bytes = obj.EncodeObjectToBytes(); - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 2); - - EXPECT_EQ(decoded.id_, obj.id_); - EXPECT_EQ(decoded.x(), obj.x()); - EXPECT_EQ(decoded.y(), obj.y()); -} - -TEST(RoomObjectEncodingTest, Type2RealWorldExample) { - // Example: Large brazier (object 0x11C) - // Position (8, 12) - - RoomObject obj(0x11C, 8, 12, 0, 0); - - auto bytes = obj.EncodeObjectToBytes(); - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); - - EXPECT_EQ(decoded.id_, 0x11C); - EXPECT_EQ(decoded.x(), 8); - EXPECT_EQ(decoded.y(), 12); -} - -// ============================================================================ -// Type 3 Object Encoding/Decoding Tests -// ============================================================================ - -TEST(RoomObjectEncodingTest, Type3EncodeDecodeChest) { - // Type3: xxxxxxii yyyyyyii 11111iii - // Example: Small chest (0xF99), position (5, 10) - - RoomObject obj(0xF99, 5, 10, 0, 0); - - // Encode - auto bytes = obj.EncodeObjectToBytes(); - - // Verify b3 >= 0xF8 - EXPECT_GE(bytes.b3, 0xF8); - - // Decode - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 0); - - // Verify - EXPECT_EQ(decoded.id_, obj.id_); - EXPECT_EQ(decoded.x(), obj.x()); - EXPECT_EQ(decoded.y(), obj.y()); -} - -TEST(RoomObjectEncodingTest, Type3EncodeDcodeBigChest) { - // Example: Big chest (0xFB1), position (15, 20) - - RoomObject obj(0xFB1, 15, 20, 0, 1); - - auto bytes = obj.EncodeObjectToBytes(); - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, 1); - - EXPECT_EQ(decoded.id_, 0xFB1); - EXPECT_EQ(decoded.x(), 15); - EXPECT_EQ(decoded.y(), 20); -} - -TEST(RoomObjectEncodingTest, Type3RealWorldExample) { - // Example from ROM: Chest at position (10, 15) - // Correct bytes for ID 0xF99: 0x29 0x3E 0xF9 - - auto decoded = RoomObject::DecodeObjectFromBytes(0x29, 0x3E, 0xF9, 0); - - // Expected: X=10, Y=15, ID=0xF99 (small chest) - EXPECT_EQ(decoded.x(), 10); - EXPECT_EQ(decoded.y(), 15); - EXPECT_EQ(decoded.id_, 0xF99); -} - -// ============================================================================ -// Edge Cases and Special Values -// ============================================================================ - -TEST(RoomObjectEncodingTest, LayerPreservation) { - // Test that layer information is preserved through encode/decode - for (uint8_t layer = 0; layer <= 2; layer++) { - RoomObject obj(0x42, 10, 20, 3, layer); - - auto bytes = obj.EncodeObjectToBytes(); - auto decoded = RoomObject::DecodeObjectFromBytes(bytes.b1, bytes.b2, bytes.b3, layer); - - EXPECT_EQ(decoded.GetLayerValue(), layer) << "Failed for layer " << (int)layer; - } -} - -TEST(RoomObjectEncodingTest, BoundaryBetweenTypes) { - // Test boundary values between object types - // NOTE: Type1 can only go up to ID 0xF7 (b3 >= 0xF8 triggers Type3) - - // Last safe Type1 object - RoomObject type1(0xF7, 10, 20, 3, 0); - auto bytes1 = type1.EncodeObjectToBytes(); - auto decoded1 = RoomObject::DecodeObjectFromBytes(bytes1.b1, bytes1.b2, bytes1.b3, 0); - EXPECT_EQ(decoded1.id_, 0xF7); - - // First Type2 object - RoomObject type2(0x100, 10, 20, 0, 0); - auto bytes2 = type2.EncodeObjectToBytes(); - auto decoded2 = RoomObject::DecodeObjectFromBytes(bytes2.b1, bytes2.b2, bytes2.b3, 0); - EXPECT_EQ(decoded2.id_, 0x100); - - // Last Type2 object - RoomObject type2_last(0x13F, 10, 20, 0, 0); - auto bytes2_last = type2_last.EncodeObjectToBytes(); - auto decoded2_last = RoomObject::DecodeObjectFromBytes(bytes2_last.b1, bytes2_last.b2, bytes2_last.b3, 0); - EXPECT_EQ(decoded2_last.id_, 0x13F); - - // Type3 objects (start at 0xF80) - RoomObject type3(0xF99, 10, 20, 0, 0); - auto bytes3 = type3.EncodeObjectToBytes(); - auto decoded3 = RoomObject::DecodeObjectFromBytes(bytes3.b1, bytes3.b2, bytes3.b3, 0); - EXPECT_EQ(decoded3.id_, 0xF99); -} - -TEST(RoomObjectEncodingTest, ZeroPosition) { - // Test objects at position (0, 0) - RoomObject type1(0x10, 0, 0, 0, 0); - auto bytes1 = type1.EncodeObjectToBytes(); - auto decoded1 = RoomObject::DecodeObjectFromBytes(bytes1.b1, bytes1.b2, bytes1.b3, 0); - EXPECT_EQ(decoded1.x(), 0); - EXPECT_EQ(decoded1.y(), 0); - - RoomObject type2(0x110, 0, 0, 0, 0); - auto bytes2 = type2.EncodeObjectToBytes(); - auto decoded2 = RoomObject::DecodeObjectFromBytes(bytes2.b1, bytes2.b2, bytes2.b3, 0); - EXPECT_EQ(decoded2.x(), 0); - EXPECT_EQ(decoded2.y(), 0); -} - -// ============================================================================ -// Batch Tests with Multiple Objects -// ============================================================================ - -TEST(RoomObjectEncodingTest, MultipleObjectsRoundTrip) { - // Test encoding/decoding a batch of different objects - std::vector objects; - - // Add various objects - objects.emplace_back(0x10, 5, 10, 2, 0); // Type1 - objects.emplace_back(0x42, 15, 20, 5, 1); // Type1 - objects.emplace_back(0x110, 8, 12, 0, 0); // Type2 - objects.emplace_back(0x125, 25, 30, 0, 1); // Type2 - objects.emplace_back(0xF99, 10, 15, 0, 0); // Type3 (chest) - objects.emplace_back(0xFB1, 20, 25, 0, 2); // Type3 (big chest) - - for (size_t i = 0; i < objects.size(); i++) { - auto& obj = objects[i]; - auto bytes = obj.EncodeObjectToBytes(); - auto decoded = RoomObject::DecodeObjectFromBytes( - bytes.b1, bytes.b2, bytes.b3, obj.GetLayerValue()); - - EXPECT_EQ(decoded.id_, obj.id_) << "Failed at index " << i; - EXPECT_EQ(decoded.x(), obj.x()) << "Failed at index " << i; - EXPECT_EQ(decoded.y(), obj.y()) << "Failed at index " << i; - if (obj.id_ < 0x100) { // Type1 objects have size - EXPECT_EQ(decoded.size(), obj.size()) << "Failed at index " << i; - } - } -} - -} // namespace -} // namespace zelda3 -} // namespace yaze -