From 405dece70aab15b0eb61d8b307665e575d0744e7 Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 5 Oct 2025 19:07:06 -0400 Subject: [PATCH] refactor: Enhance Map Properties System with Graphics Management and Refresh Logic - Introduced OverworldGraphicsManager to handle graphics loading, refreshing, and texture management for the overworld editor. - Updated MapPropertiesSystem to integrate new graphics management features, ensuring proper refresh order for map properties and graphics. - Added methods for forcing graphics refresh and handling sibling map graphics, improving the responsiveness of the editor. - Enhanced UI elements in the MapPropertiesSystem for better user experience, including tooltips and organized layouts. - Updated CMake configuration to include new graphics manager files, ensuring proper integration into the build system. --- src/app/editor/editor_library.cmake | 1 + src/app/editor/overworld/map_properties.cc | 214 ++++-- src/app/editor/overworld/map_properties.h | 14 +- src/app/editor/overworld/overworld_editor.cc | 108 ++- src/app/editor/overworld/overworld_editor.h | 2 + .../overworld/overworld_graphics_manager.cc | 714 ++++++++++++++++++ .../overworld/overworld_graphics_manager.h | 204 +++++ src/app/zelda3/overworld/overworld.cc | 8 - src/app/zelda3/overworld/overworld_exit.h | 4 - src/app/zelda3/overworld/overworld_item.h | 6 - 10 files changed, 1190 insertions(+), 85 deletions(-) create mode 100644 src/app/editor/overworld/overworld_graphics_manager.cc create mode 100644 src/app/editor/overworld/overworld_graphics_manager.h diff --git a/src/app/editor/editor_library.cmake b/src/app/editor/editor_library.cmake index e2593569..cb6b8ec9 100644 --- a/src/app/editor/editor_library.cmake +++ b/src/app/editor/editor_library.cmake @@ -36,6 +36,7 @@ set( app/editor/graphics/gfx_group_editor.cc app/editor/overworld/entity.cc app/editor/overworld/overworld_entity_renderer.cc + app/editor/overworld/overworld_graphics_manager.cc app/editor/system/settings_editor.cc app/editor/system/command_manager.cc app/editor/system/extension_manager.cc diff --git a/src/app/editor/overworld/map_properties.cc b/src/app/editor/overworld/map_properties.cc index 1d931ee2..a9c48a32 100644 --- a/src/app/editor/overworld/map_properties.cc +++ b/src/app/editor/overworld/map_properties.cc @@ -460,7 +460,22 @@ void MapPropertiesSystem::DrawGraphicsPopup(int current_map, int game_state) { overworld_->mutable_overworld_map(current_map) ->mutable_area_graphics(), kHexByteInputWidth)) { + // CORRECT ORDER: Properties first, then graphics reload + + // 1. Propagate properties to siblings FIRST (calls LoadAreaGraphics on siblings) RefreshMapProperties(); + + // 2. Force immediate refresh of current map + (*maps_bmp_)[current_map].set_modified(true); + overworld_->mutable_overworld_map(current_map)->LoadAreaGraphics(); + + // 3. Refresh siblings immediately + RefreshSiblingMapGraphics(current_map); + + // 4. Update tile selector + RefreshTile16Blockset(); + + // 5. Final refresh RefreshOverworldMap(); } HOVER_HINT("Main tileset graphics for this map area"); @@ -472,6 +487,7 @@ void MapPropertiesSystem::DrawGraphicsPopup(int current_map, int game_state) { overworld_->mutable_overworld_map(current_map) ->mutable_sprite_graphics(game_state), kHexByteInputWidth)) { + ForceRefreshGraphics(current_map); RefreshMapProperties(); RefreshOverworldMap(); } @@ -484,34 +500,48 @@ void MapPropertiesSystem::DrawGraphicsPopup(int current_map, int game_state) { overworld_->mutable_overworld_map(current_map) ->mutable_animated_gfx(), kHexByteInputWidth)) { + ForceRefreshGraphics(current_map); RefreshMapProperties(); + RefreshTile16Blockset(); RefreshOverworldMap(); } HOVER_HINT("Animated tile graphics (water, lava, etc.)"); } - ImGui::Separator(); - ImGui::Text(ICON_MD_GRID_VIEW " Custom Tile Graphics"); - ImGui::Separator(); + // Custom Tile Graphics - Only available for v1+ ROMs + if (asm_version >= 1 && asm_version != 0xFF) { + ImGui::Separator(); + ImGui::Text(ICON_MD_GRID_VIEW " Custom Tile Graphics"); + ImGui::Separator(); - // Show the 8 custom graphics IDs in a 2-column layout for density - if (BeginTable("CustomTileGraphics", 2, - ImGuiTableFlags_SizingFixedFit)) { - for (int i = 0; i < 8; i++) { - TableNextColumn(); - std::string label = absl::StrFormat(ICON_MD_LAYERS " Sheet %d", i); - if (gui::InputHexByte(label.c_str(), - overworld_->mutable_overworld_map(current_map) - ->mutable_custom_tileset(i), - 90.f)) { - RefreshMapProperties(); - RefreshOverworldMap(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Custom graphics sheet %d (0x00-0xFF)", i); + // Show the 8 custom graphics IDs in a 2-column layout for density + if (BeginTable("CustomTileGraphics", 2, + ImGuiTableFlags_SizingFixedFit)) { + for (int i = 0; i < 8; i++) { + TableNextColumn(); + std::string label = absl::StrFormat(ICON_MD_LAYERS " Sheet %d", i); + if (gui::InputHexByte(label.c_str(), + overworld_->mutable_overworld_map(current_map) + ->mutable_custom_tileset(i), + 90.f)) { + ForceRefreshGraphics(current_map); + RefreshMapProperties(); + RefreshTile16Blockset(); + RefreshOverworldMap(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Custom graphics sheet %d (0x00-0xFF)", i); + } } + ImGui::EndTable(); } - ImGui::EndTable(); + } else if (asm_version == 0xFF) { + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), + ICON_MD_INFO " Custom Tile Graphics"); + ImGui::TextWrapped( + "Custom tile graphics require ZSCustomOverworld v1+.\n" + "Upgrade your ROM to access 8 customizable graphics sheets."); } ImGui::PopStyleVar(2); // Pop the 2 style variables we pushed @@ -693,7 +723,12 @@ void MapPropertiesSystem::DrawBasicPropertiesTab(int current_map) { overworld_->mutable_overworld_map(current_map) ->mutable_area_graphics(), kInputFieldSize)) { + // CORRECT ORDER: Properties first, then graphics reload RefreshMapProperties(); + (*maps_bmp_)[current_map].set_modified(true); + overworld_->mutable_overworld_map(current_map)->LoadAreaGraphics(); + RefreshSiblingMapGraphics(current_map); + RefreshTile16Blockset(); RefreshOverworldMap(); } if (ImGui::IsItemHovered()) { @@ -955,36 +990,55 @@ void MapPropertiesSystem::DrawCustomFeaturesTab(int current_map) { } void MapPropertiesSystem::DrawTileGraphicsTab(int current_map) { - ImGui::Text(ICON_MD_GRID_VIEW " Custom Tile Graphics (8 sheets)"); - Separator(); - - if (BeginTable("TileGraphics", 2, - ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { - ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 180); - ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); - - for (int i = 0; i < 8; i++) { - TableNextColumn(); - ImGui::Text(ICON_MD_LAYERS " Sheet %d", i); - TableNextColumn(); - if (gui::InputHexByte(absl::StrFormat("##TileGfx%d", i).c_str(), - overworld_->mutable_overworld_map(current_map) - ->mutable_custom_tileset(i), - kInputFieldSize)) { - RefreshMapProperties(); - RefreshOverworldMap(); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Custom graphics sheet %d (0x00-0xFF)", i); - } - } - - ImGui::EndTable(); - } + static uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; - Separator(); - ImGui::TextWrapped("These 8 sheets allow custom tile graphics per map. " - "Each sheet references a graphics ID loaded into VRAM."); + // Only show custom tile graphics for v1+ ROMs + if (asm_version >= 1 && asm_version != 0xFF) { + ImGui::Text(ICON_MD_GRID_VIEW " Custom Tile Graphics (8 sheets)"); + Separator(); + + if (BeginTable("TileGraphics", 2, + ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("Property", ImGuiTableColumnFlags_WidthFixed, 180); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + + for (int i = 0; i < 8; i++) { + TableNextColumn(); + ImGui::Text(ICON_MD_LAYERS " Sheet %d", i); + TableNextColumn(); + if (gui::InputHexByte(absl::StrFormat("##TileGfx%d", i).c_str(), + overworld_->mutable_overworld_map(current_map) + ->mutable_custom_tileset(i), + kInputFieldSize)) { + overworld_->mutable_overworld_map(current_map)->LoadAreaGraphics(); + ForceRefreshGraphics(current_map); + RefreshSiblingMapGraphics(current_map); + RefreshMapProperties(); + RefreshTile16Blockset(); + RefreshOverworldMap(); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Custom graphics sheet %d (0x00-0xFF)", i); + } + } + + ImGui::EndTable(); + } + + Separator(); + ImGui::TextWrapped("These 8 sheets allow custom tile graphics per map. " + "Each sheet references a graphics ID loaded into VRAM."); + } else { + // Vanilla ROM - show info message + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), + ICON_MD_INFO " Custom Tile Graphics"); + ImGui::Separator(); + ImGui::TextWrapped( + "Custom tile graphics are not available in vanilla ROMs.\n\n" + "To enable this feature, upgrade your ROM to ZSCustomOverworld v1+, " + "which provides 8 customizable graphics sheets per map for advanced " + "tileset customization."); + } } void MapPropertiesSystem::DrawMusicTab(int current_map) { @@ -1090,6 +1144,70 @@ absl::Status MapPropertiesSystem::RefreshMapPalette() { return absl::OkStatus(); } +absl::Status MapPropertiesSystem::RefreshTile16Blockset() { + if (refresh_tile16_blockset_) { + return refresh_tile16_blockset_(); + } + return absl::OkStatus(); +} + +void MapPropertiesSystem::ForceRefreshGraphics(int map_index) { + if (force_refresh_graphics_) { + force_refresh_graphics_(map_index); + } +} + +void MapPropertiesSystem::RefreshSiblingMapGraphics(int map_index, bool include_self) { + if (!overworld_ || !maps_bmp_ || map_index < 0 || map_index >= zelda3::kNumOverworldMaps) { + return; + } + + auto* map = overworld_->mutable_overworld_map(map_index); + if (map->area_size() == zelda3::AreaSizeEnum::SmallArea) { + return; // No siblings for small areas + } + + int parent_id = map->parent(); + std::vector siblings; + + switch (map->area_size()) { + case zelda3::AreaSizeEnum::LargeArea: + siblings = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9}; + break; + case zelda3::AreaSizeEnum::WideArea: + siblings = {parent_id, parent_id + 1}; + break; + case zelda3::AreaSizeEnum::TallArea: + siblings = {parent_id, parent_id + 8}; + break; + default: + return; + } + + for (int sibling : siblings) { + if (sibling >= 0 && sibling < zelda3::kNumOverworldMaps) { + // Skip self unless include_self is true + if (sibling == map_index && !include_self) { + continue; + } + + // Mark as modified FIRST + (*maps_bmp_)[sibling].set_modified(true); + + // Load graphics from ROM + overworld_->mutable_overworld_map(sibling)->LoadAreaGraphics(); + + // CRITICAL FIX: Force immediate refresh on the sibling + // This will trigger the callback to OverworldEditor's RefreshChildMapOnDemand + ForceRefreshGraphics(sibling); + } + } + + // After marking all siblings, trigger a refresh + // This ensures all marked maps get processed + RefreshOverworldMap(); +} + void MapPropertiesSystem::DrawMosaicControls(int current_map) { static uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; diff --git a/src/app/editor/overworld/map_properties.h b/src/app/editor/overworld/map_properties.h index 4c542562..93c5b404 100644 --- a/src/app/editor/overworld/map_properties.h +++ b/src/app/editor/overworld/map_properties.h @@ -22,6 +22,7 @@ class MapPropertiesSystem { // Callback types for refresh operations using RefreshCallback = std::function; using RefreshPaletteCallback = std::function; + using ForceRefreshGraphicsCallback = std::function; explicit MapPropertiesSystem(zelda3::Overworld* overworld, Rom* rom, std::array* maps_bmp = nullptr, @@ -31,10 +32,14 @@ class MapPropertiesSystem { // Set callbacks for refresh operations void SetRefreshCallbacks(RefreshCallback refresh_map_properties, RefreshCallback refresh_overworld_map, - RefreshPaletteCallback refresh_map_palette) { + RefreshPaletteCallback refresh_map_palette, + RefreshPaletteCallback refresh_tile16_blockset = nullptr, + ForceRefreshGraphicsCallback force_refresh_graphics = nullptr) { refresh_map_properties_ = std::move(refresh_map_properties); refresh_overworld_map_ = std::move(refresh_overworld_map); refresh_map_palette_ = std::move(refresh_map_palette); + refresh_tile16_blockset_ = std::move(refresh_tile16_blockset); + force_refresh_graphics_ = std::move(force_refresh_graphics); } // Main interface methods @@ -85,6 +90,11 @@ class MapPropertiesSystem { void RefreshMapProperties(); void RefreshOverworldMap(); absl::Status RefreshMapPalette(); + absl::Status RefreshTile16Blockset(); + void ForceRefreshGraphics(int map_index); + + // Helper to refresh sibling map graphics for multi-area maps + void RefreshSiblingMapGraphics(int map_index, bool include_self = false); zelda3::Overworld* overworld_; Rom* rom_; @@ -95,6 +105,8 @@ class MapPropertiesSystem { RefreshCallback refresh_map_properties_; RefreshCallback refresh_overworld_map_; RefreshPaletteCallback refresh_map_palette_; + RefreshPaletteCallback refresh_tile16_blockset_; + ForceRefreshGraphicsCallback force_refresh_graphics_; // Using centralized UI constants from ui_constants.h }; diff --git a/src/app/editor/overworld/overworld_editor.cc b/src/app/editor/overworld/overworld_editor.cc index c13f8c3c..6af17928 100644 --- a/src/app/editor/overworld/overworld_editor.cc +++ b/src/app/editor/overworld/overworld_editor.cc @@ -54,7 +54,9 @@ void OverworldEditor::Initialize() { map_properties_system_->SetRefreshCallbacks( [this]() { this->RefreshMapProperties(); }, [this]() { this->RefreshOverworldMap(); }, - [this]() -> absl::Status { return this->RefreshMapPalette(); } + [this]() -> absl::Status { return this->RefreshMapPalette(); }, + [this]() -> absl::Status { return this->RefreshTile16Blockset(); }, + [this](int map_index) { this->ForceRefreshGraphics(map_index); } ); // Initialize OverworldEditorManager for v3 features @@ -343,18 +345,30 @@ void OverworldEditor::DrawToolset() { toolbar.AddSeparator(); // Inline map properties with icon labels - use toolbar methods for consistency - if (toolbar.AddProperty(ICON_MD_IMAGE, "##gfx", + if (toolbar.AddProperty(ICON_MD_IMAGE, " Gfx", overworld_.mutable_overworld_map(current_map_)->mutable_area_graphics(), [this]() { + // CORRECT ORDER: Properties first, then graphics reload + + // 1. Propagate properties to siblings FIRST (this also calls LoadAreaGraphics on siblings) RefreshMapProperties(); - RefreshOverworldMap(); + + // 2. Force immediate refresh of current map and all siblings + maps_bmp_[current_map_].set_modified(true); + RefreshChildMapOnDemand(current_map_); + RefreshSiblingMapGraphics(current_map_); + + // 3. Update tile selector + RefreshTile16Blockset(); })) { // Property changed } - if (toolbar.AddProperty(ICON_MD_PALETTE, "##pal", + if (toolbar.AddProperty(ICON_MD_PALETTE, " Pal", overworld_.mutable_overworld_map(current_map_)->mutable_area_palette(), [this]() { + // Palette changes also need to propagate to siblings + RefreshSiblingMapGraphics(current_map_); RefreshMapProperties(); status_ = RefreshMapPalette(); RefreshOverworldMap(); @@ -406,13 +420,6 @@ void OverworldEditor::DrawToolset() { show_gfx_groups_ = !show_gfx_groups_; } - toolbar.AddSeparator(); - - // v3 Settings and Usage Statistics - toolbar.AddV3StatusBadge(asm_version, [this]() { - show_v3_settings_ = !show_v3_settings_; - }); - if (toolbar.AddUsageStatsButton("Open Usage Statistics")) { show_usage_stats_ = !show_usage_stats_; } @@ -1639,13 +1646,11 @@ void OverworldEditor::RefreshChildMapOnDemand(int map_index) { map_index); } - // Update texture on main thread - if (maps_bmp_[map_index].texture()) { - Renderer::Get().UpdateBitmap(&maps_bmp_[map_index]); - } else { - // Create texture if it doesn't exist - EnsureMapTexture(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]); } // Handle multi-area maps (large, wide, tall) with safe coordination @@ -1888,6 +1893,67 @@ absl::Status OverworldEditor::RefreshMapPalette() { return absl::OkStatus(); } +void OverworldEditor::ForceRefreshGraphics(int map_index) { + // Mark the bitmap as modified to force refresh on next update + if (map_index >= 0 && map_index < static_cast(maps_bmp_.size())) { + maps_bmp_[map_index].set_modified(true); + + // Clear blockset cache + current_blockset_ = 0xFF; + + LOG_INFO("OverworldEditor", "ForceRefreshGraphics: Map %d marked for refresh", map_index); + } +} + +void OverworldEditor::RefreshSiblingMapGraphics(int map_index, bool include_self) { + if (map_index < 0 || map_index >= static_cast(maps_bmp_.size())) { + return; + } + + auto* map = overworld_.mutable_overworld_map(map_index); + if (map->area_size() == zelda3::AreaSizeEnum::SmallArea) { + return; // No siblings for small areas + } + + int parent_id = map->parent(); + std::vector siblings; + + switch (map->area_size()) { + case zelda3::AreaSizeEnum::LargeArea: + siblings = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9}; + break; + case zelda3::AreaSizeEnum::WideArea: + siblings = {parent_id, parent_id + 1}; + break; + case zelda3::AreaSizeEnum::TallArea: + siblings = {parent_id, parent_id + 8}; + break; + default: + return; + } + + for (int sibling : siblings) { + if (sibling >= 0 && sibling < 0xA0) { + // Skip self unless include_self is true + if (sibling == map_index && !include_self) { + continue; + } + + // Mark as modified FIRST before loading + maps_bmp_[sibling].set_modified(true); + + // Load graphics from ROM + overworld_.mutable_overworld_map(sibling)->LoadAreaGraphics(); + + // CRITICAL FIX: Bypass visibility check - force immediate refresh + // Call RefreshChildMapOnDemand() directly instead of RefreshOverworldMapOnDemand() + RefreshChildMapOnDemand(sibling); + + LOG_INFO("OverworldEditor", "RefreshSiblingMapGraphics: Refreshed sibling map %d", sibling); + } + } +} + void OverworldEditor::RefreshMapProperties() { const auto& current_ow_map = *overworld_.mutable_overworld_map(current_map_); @@ -1935,6 +2001,9 @@ void OverworldEditor::RefreshMapProperties() { map.set_sprite_palette(game_state_, current_ow_map.sprite_palette(game_state_)); map.set_message_id(current_ow_map.message_id()); + + // CRITICAL FIX: Reload graphics after changing properties + map.LoadAreaGraphics(); } } } else { @@ -1954,6 +2023,9 @@ void OverworldEditor::RefreshMapProperties() { map.set_sprite_palette(game_state_, current_ow_map.sprite_palette(game_state_)); map.set_message_id(current_ow_map.message_id()); + + // CRITICAL FIX: Reload graphics after changing properties + map.LoadAreaGraphics(); } } } diff --git a/src/app/editor/overworld/overworld_editor.h b/src/app/editor/overworld/overworld_editor.h index 0d894c29..4750c1a9 100644 --- a/src/app/editor/overworld/overworld_editor.h +++ b/src/app/editor/overworld/overworld_editor.h @@ -123,6 +123,8 @@ class OverworldEditor : public Editor, public gfx::GfxContext { absl::Status RefreshMapPalette(); void RefreshMapProperties(); absl::Status RefreshTile16Blockset(); + void ForceRefreshGraphics(int map_index); + void RefreshSiblingMapGraphics(int map_index, bool include_self = false); void DrawOverworldMaps(); void DrawOverworldEdits(); diff --git a/src/app/editor/overworld/overworld_graphics_manager.cc b/src/app/editor/overworld/overworld_graphics_manager.cc new file mode 100644 index 00000000..9cba6963 --- /dev/null +++ b/src/app/editor/overworld/overworld_graphics_manager.cc @@ -0,0 +1,714 @@ +#include "overworld_graphics_manager.h" + +#include + +#include "app/core/features.h" +#include "app/core/window.h" +#include "app/gfx/performance_profiler.h" +#include "util/log.h" +#include "util/macro.h" + +namespace yaze { +namespace editor { + +using core::Renderer; +using zelda3::kNumOverworldMaps; +using zelda3::kOverworldMapSize; + +constexpr int kTile16Size = 16; + +// ============================================================================ +// Loading Operations +// ============================================================================ + +absl::Status OverworldGraphicsManager::LoadGraphics() { + gfx::ScopedTimer timer("LoadGraphics"); + + LOG_INFO("OverworldGraphicsManager", "Loading overworld."); + // Load the Link to the Past overworld. + { + gfx::ScopedTimer load_timer("Overworld::Load"); + RETURN_IF_ERROR(overworld_->Load(rom_)); + } + *palette_ = overworld_->current_area_palette(); + + LOG_INFO("OverworldGraphicsManager", "Loading overworld graphics (optimized)."); + + // Phase 1: Create bitmaps without textures for faster loading + // 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_); + } + + LOG_INFO("OverworldGraphicsManager", "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_); + } + map_blockset_loaded_ = true; + + // Copy the tile16 data into individual tiles. + auto tile16_blockset_data = overworld_->tile16_blockset_data(); + LOG_INFO("OverworldGraphicsManager", "Loading overworld tile16 graphics."); + + { + gfx::ScopedTimer tilemap_timer("CreateTilemap"); + *tile16_blockset_ = + gfx::CreateTilemap(tile16_blockset_data, 0x80, 0x2000, kTile16Size, + zelda3::kNumTile16Individual, *palette_); + } + + // Phase 2: Create bitmaps only for essential maps initially + // Non-essential maps will be created on-demand when accessed + constexpr int kEssentialMapsPerWorld = 8; + constexpr int kLightWorldEssential = kEssentialMapsPerWorld; + constexpr int kDarkWorldEssential = + zelda3::kDarkWorldMapIdStart + kEssentialMapsPerWorld; + constexpr int kSpecialWorldEssential = + zelda3::kSpecialWorldMapIdStart + kEssentialMapsPerWorld; + + LOG_INFO("OverworldGraphicsManager", + "Creating bitmaps for essential maps only (first %d maps per world)", + kEssentialMapsPerWorld); + + std::vector maps_to_texture; + maps_to_texture.reserve(kEssentialMapsPerWorld * + 3); // 8 maps per world * 3 worlds + + { + gfx::ScopedTimer maps_timer("CreateEssentialOverworldMaps"); + for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) { + bool is_essential = false; + + // Check if this is an essential map + if (i < kLightWorldEssential) { + is_essential = true; + } else if (i >= zelda3::kDarkWorldMapIdStart && i < kDarkWorldEssential) { + is_essential = true; + } else if (i >= zelda3::kSpecialWorldMapIdStart && + i < kSpecialWorldEssential) { + is_essential = true; + } + + if (is_essential) { + overworld_->set_current_map(i); + auto palette = overworld_->current_area_palette(); + try { + // Create bitmap data and surface but defer texture creation + (*maps_bmp_)[i].Create(kOverworldMapSize, kOverworldMapSize, 0x80, + overworld_->current_map_bitmap_data()); + (*maps_bmp_)[i].SetPalette(palette); + maps_to_texture.push_back(&(*maps_bmp_)[i]); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OverworldGraphicsManager", "Error allocating map %d: %s", + i, e.what()); + continue; + } + } + // Non-essential maps will be created on-demand when accessed + } + } + + // Phase 3: Create textures only for currently visible maps + // Only create textures for the first few maps initially + const int initial_texture_count = + std::min(4, static_cast(maps_to_texture.size())); + { + gfx::ScopedTimer initial_textures_timer("CreateInitialTextures"); + for (int i = 0; i < initial_texture_count; ++i) { + Renderer::Get().RenderBitmap(maps_to_texture[i]); + } + } + + // Store remaining maps for lazy texture creation + deferred_map_textures_.assign(maps_to_texture.begin() + initial_texture_count, + maps_to_texture.end()); + + if (core::FeatureFlags::get().overworld.kDrawOverworldSprites) { + { + gfx::ScopedTimer sprites_timer("LoadSpriteGraphics"); + RETURN_IF_ERROR(LoadSpriteGraphics()); + } + } + + all_gfx_loaded_ = true; + return absl::OkStatus(); +} + +absl::Status OverworldGraphicsManager::LoadSpriteGraphics() { + // Render the sprites for each Overworld map + const int depth = 0x10; + for (int i = 0; i < 3; i++) + for (auto const& sprite : *overworld_->mutable_sprites(i)) { + int width = sprite.width(); + int height = sprite.height(); + if (width == 0 || height == 0) { + continue; + } + if (sprite_previews_->size() < sprite.id()) { + sprite_previews_->resize(sprite.id() + 1); + } + (*sprite_previews_)[sprite.id()].Create(width, height, depth, + *sprite.preview_graphics()); + (*sprite_previews_)[sprite.id()].SetPalette(*palette_); + Renderer::Get().RenderBitmap(&(*sprite_previews_)[sprite.id()]); + } + return absl::OkStatus(); +} + +// ============================================================================ +// Texture Management +// ============================================================================ + +void OverworldGraphicsManager::ProcessDeferredTextures() { + std::lock_guard lock(deferred_textures_mutex_); + + // Always process deferred textures progressively, even if the list is "empty" + // This allows for continuous background loading + + // PHASE 1: Priority loading for current world + const int high_priority_per_frame = 4; // Current world maps + const int low_priority_per_frame = 2; // Other world maps + const int refresh_per_frame = 2; // Modified map refreshes + int processed = 0; + + // Process high-priority deferred textures (current world) + if (!deferred_map_textures_.empty()) { + auto it = deferred_map_textures_.begin(); + while (it != deferred_map_textures_.end() && processed < high_priority_per_frame) { + if (*it && !(*it)->texture()) { + // Find map index for priority check + int map_index = -1; + for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) { + if (&(*maps_bmp_)[i] == *it) { + map_index = i; + break; + } + } + + bool is_current_world = false; + if (map_index >= 0) { + int map_world = map_index / 0x40; // 64 maps per world + is_current_world = (map_world == current_world_); + } + + if (is_current_world) { + Renderer::Get().RenderBitmap(*it); + processed++; + it = deferred_map_textures_.erase(it); + } else { + ++it; + } + } else { + ++it; + } + } + } + + // PHASE 2: Background loading for other worlds (lower priority) + if (!deferred_map_textures_.empty() && processed < high_priority_per_frame) { + auto it = deferred_map_textures_.begin(); + int low_priority_processed = 0; + while (it != deferred_map_textures_.end() && low_priority_processed < low_priority_per_frame) { + if (*it && !(*it)->texture()) { + Renderer::Get().RenderBitmap(*it); + low_priority_processed++; + processed++; + it = deferred_map_textures_.erase(it); + } else { + ++it; + } + } + } + + // PHASE 3: Process modified maps that need refresh (highest priority) + int refresh_processed = 0; + for (int i = 0; i < zelda3::kNumOverworldMaps && refresh_processed < refresh_per_frame; ++i) { + if ((*maps_bmp_)[i].modified() && (*maps_bmp_)[i].is_active()) { + // Check if this map is in current world (high priority) or visible + bool is_current_world = (i / 0x40 == current_world_); + bool is_current_map = (i == current_map_); + + if (is_current_map || is_current_world) { + RefreshOverworldMapOnDemand(i); + refresh_processed++; + } + } + } + + // PHASE 4: Background refresh for modified maps in other worlds (very low priority) + if (refresh_processed == 0) { + for (int i = 0; i < zelda3::kNumOverworldMaps; ++i) { + if ((*maps_bmp_)[i].modified() && (*maps_bmp_)[i].is_active()) { + bool is_current_world = (i / 0x40 == current_world_); + if (!is_current_world) { + // Just mark for later, don't refresh now to avoid lag + // These will be refreshed when the world is switched + break; + } + } + } + } +} + +void OverworldGraphicsManager::EnsureMapTexture(int map_index) { + if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) { + return; + } + + // Ensure the map is built first (on-demand loading) + auto status = overworld_->EnsureMapBuilt(map_index); + if (!status.ok()) { + LOG_ERROR("OverworldGraphicsManager", "Failed to build map %d: %s", map_index, + status.message().data()); + return; + } + + auto& bitmap = (*maps_bmp_)[map_index]; + + // If bitmap doesn't exist yet (non-essential map), create it now + if (!bitmap.is_active()) { + overworld_->set_current_map(map_index); + auto palette = overworld_->current_area_palette(); + try { + bitmap.Create(kOverworldMapSize, kOverworldMapSize, 0x80, + overworld_->current_map_bitmap_data()); + bitmap.SetPalette(palette); + } catch (const std::bad_alloc& e) { + LOG_ERROR("OverworldGraphicsManager", "Error allocating bitmap for map %d: %s", + map_index, e.what()); + return; + } + } + + if (!bitmap.texture() && bitmap.is_active()) { + Renderer::Get().RenderBitmap(&bitmap); + + // Remove from deferred list if it was there + std::lock_guard lock(deferred_textures_mutex_); + auto it = std::find(deferred_map_textures_.begin(), + deferred_map_textures_.end(), &bitmap); + if (it != deferred_map_textures_.end()) { + deferred_map_textures_.erase(it); + } + } +} + +// ============================================================================ +// Refresh Operations +// ============================================================================ + +void OverworldGraphicsManager::RefreshOverworldMap() { + // Use the new on-demand refresh system + RefreshOverworldMapOnDemand(current_map_); +} + +void OverworldGraphicsManager::RefreshOverworldMapOnDemand(int map_index) { + if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) { + return; + } + + // Check if the map is actually visible or being edited + bool is_current_map = (map_index == current_map_); + bool is_current_world = (map_index / 0x40 == current_world_); + + // For non-current maps in non-current worlds, defer the refresh + if (!is_current_map && !is_current_world) { + // Mark for deferred refresh - will be processed when the map becomes visible + (*maps_bmp_)[map_index].set_modified(true); + return; + } + + // For visible maps, do immediate refresh + RefreshChildMapOnDemand(map_index); +} + +void OverworldGraphicsManager::RefreshChildMap(int map_index) { + overworld_->mutable_overworld_map(map_index)->LoadAreaGraphics(); + auto status = overworld_->mutable_overworld_map(map_index)->BuildTileset(); + PRINT_IF_ERROR(status); + status = overworld_->mutable_overworld_map(map_index)->BuildTiles16Gfx( + *overworld_->mutable_tiles16(), overworld_->tiles16().size()); + PRINT_IF_ERROR(status); + status = overworld_->mutable_overworld_map(map_index)->BuildBitmap( + overworld_->GetMapTiles(current_world_)); + (*maps_bmp_)[map_index].set_data( + overworld_->mutable_overworld_map(map_index)->bitmap_data()); + (*maps_bmp_)[map_index].set_modified(true); + PRINT_IF_ERROR(status); +} + +void OverworldGraphicsManager::RefreshChildMapOnDemand(int map_index) { + auto* map = overworld_->mutable_overworld_map(map_index); + + // Check what actually needs to be refreshed + bool needs_graphics_rebuild = (*maps_bmp_)[map_index].modified(); + + if (needs_graphics_rebuild) { + // Only rebuild what's actually changed + map->LoadAreaGraphics(); + + // Rebuild tileset only if graphics changed + auto status = map->BuildTileset(); + if (!status.ok()) { + LOG_ERROR("OverworldGraphicsManager", "Failed to build tileset for map %d: %s", + map_index, status.message().data()); + return; + } + + // Rebuild tiles16 graphics + status = map->BuildTiles16Gfx(*overworld_->mutable_tiles16(), + overworld_->tiles16().size()); + if (!status.ok()) { + LOG_ERROR("OverworldGraphicsManager", "Failed to build tiles16 graphics for map %d: %s", + map_index, status.message().data()); + return; + } + + // Rebuild bitmap + status = map->BuildBitmap(overworld_->GetMapTiles(current_world_)); + if (!status.ok()) { + LOG_ERROR("OverworldGraphicsManager", "Failed to build bitmap for map %d: %s", + map_index, status.message().data()); + return; + } + + // Update bitmap data + (*maps_bmp_)[map_index].set_data(map->bitmap_data()); + (*maps_bmp_)[map_index].set_modified(false); + + // Validate surface synchronization to help debug crashes + if (!(*maps_bmp_)[map_index].ValidateDataSurfaceSync()) { + LOG_WARN("OverworldGraphicsManager", "Warning: Surface synchronization issue detected for map %d", + map_index); + } + + // Update texture on main thread + if ((*maps_bmp_)[map_index].texture()) { + Renderer::Get().UpdateBitmap(&(*maps_bmp_)[map_index]); + } else { + // Create texture if it doesn't exist + EnsureMapTexture(map_index); + } + } + + // Handle multi-area maps (large, wide, tall) with safe coordination + // Check if ZSCustomOverworld v3 is present + uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; + bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF); + + if (use_v3_area_sizes) { + // Use v3 multi-area coordination + RefreshMultiAreaMapsSafely(map_index, map); + } else { + // Legacy logic: only handle large maps for vanilla/v2 + if (map->is_large_map()) { + RefreshMultiAreaMapsSafely(map_index, map); + } + } +} + +void OverworldGraphicsManager::RefreshMultiAreaMapsSafely( + int map_index, zelda3::OverworldMap* map) { + using zelda3::AreaSizeEnum; + + // Skip if this is already a processed sibling to avoid double-processing + static std::set currently_processing; + if (currently_processing.count(map_index)) { + return; + } + + auto area_size = map->area_size(); + if (area_size == AreaSizeEnum::SmallArea) { + return; // No siblings to coordinate + } + + LOG_DEBUG("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Processing %s area map %d (parent: %d)", + (area_size == AreaSizeEnum::LargeArea) ? "large" + : (area_size == AreaSizeEnum::WideArea) ? "wide" + : "tall", + map_index, map->parent()); + + // Determine all maps that are part of this multi-area structure + std::vector sibling_maps; + int parent_id = map->parent(); + + // Use the same logic as ZScream for area coordination + switch (area_size) { + case AreaSizeEnum::LargeArea: { + // Large Area: 2x2 grid (4 maps total) + sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9}; + LOG_DEBUG("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Large area siblings: %d, %d, %d, %d", + parent_id, parent_id + 1, parent_id + 8, parent_id + 9); + break; + } + + case AreaSizeEnum::WideArea: { + // Wide Area: 2x1 grid (2 maps total, horizontally adjacent) + sibling_maps = {parent_id, parent_id + 1}; + LOG_DEBUG("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Wide area siblings: %d, %d", + parent_id, parent_id + 1); + break; + } + + case AreaSizeEnum::TallArea: { + // Tall Area: 1x2 grid (2 maps total, vertically adjacent) + sibling_maps = {parent_id, parent_id + 8}; + LOG_DEBUG("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Tall area siblings: %d, %d", + parent_id, parent_id + 8); + break; + } + + default: + LOG_WARN("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Unknown area size %d for map %d", + static_cast(area_size), map_index); + return; + } + + // Mark all siblings as being processed to prevent recursion + for (int sibling : sibling_maps) { + currently_processing.insert(sibling); + } + + // Only refresh siblings that are visible/current and need updating + for (int sibling : sibling_maps) { + if (sibling == map_index) { + continue; // Skip self (already processed above) + } + + // Bounds check + if (sibling < 0 || sibling >= zelda3::kNumOverworldMaps) { + continue; + } + + // Only refresh if it's visible or current + bool is_current_map = (sibling == current_map_); + bool is_current_world = (sibling / 0x40 == current_world_); + bool needs_refresh = (*maps_bmp_)[sibling].modified(); + + if ((is_current_map || is_current_world) && needs_refresh) { + LOG_DEBUG("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Refreshing %s area sibling map %d " + "(parent: %d)", + (area_size == AreaSizeEnum::LargeArea) ? "large" + : (area_size == AreaSizeEnum::WideArea) ? "wide" + : "tall", + sibling, parent_id); + + // Direct refresh without calling RefreshChildMapOnDemand to avoid recursion + auto* sibling_map = overworld_->mutable_overworld_map(sibling); + if (sibling_map && (*maps_bmp_)[sibling].modified()) { + sibling_map->LoadAreaGraphics(); + + if (auto status = sibling_map->BuildTileset(); !status.ok()) { + LOG_ERROR("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Failed to refresh sibling map %d: %s", + sibling, status.message().data()); + continue; + } + + if (auto status = sibling_map->BuildTiles16Gfx(*overworld_->mutable_tiles16(), + overworld_->tiles16().size()); !status.ok()) { + LOG_ERROR("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Failed to build tiles16 graphics for sibling map %d: %s", + sibling, status.message().data()); + continue; + } + + if (auto status = sibling_map->LoadPalette(); !status.ok()) { + LOG_ERROR("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Failed to load palette for sibling map %d: %s", + sibling, status.message().data()); + continue; + } + + if (auto status = sibling_map->BuildBitmap(overworld_->GetMapTiles(current_world_)); status.ok()) { + (*maps_bmp_)[sibling].set_data(sibling_map->bitmap_data()); + (*maps_bmp_)[sibling].SetPalette(overworld_->current_area_palette()); + (*maps_bmp_)[sibling].set_modified(false); + + // Update texture if it exists + if ((*maps_bmp_)[sibling].texture()) { + Renderer::Get().UpdateBitmap(&(*maps_bmp_)[sibling]); + } else { + EnsureMapTexture(sibling); + } + } else { + LOG_ERROR("OverworldGraphicsManager", + "RefreshMultiAreaMapsSafely: Failed to build bitmap for sibling map %d: %s", + sibling, status.message().data()); + } + } + } else if (!is_current_map && !is_current_world) { + // Mark non-visible siblings for deferred refresh + (*maps_bmp_)[sibling].set_modified(true); + } + } + + // Clear processing set after completion + for (int sibling : sibling_maps) { + currently_processing.erase(sibling); + } +} + +absl::Status OverworldGraphicsManager::RefreshMapPalette() { + RETURN_IF_ERROR( + overworld_->mutable_overworld_map(current_map_)->LoadPalette()); + const auto current_map_palette = overworld_->current_area_palette(); + + // Check if ZSCustomOverworld v3 is present + uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; + bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF); + + if (use_v3_area_sizes) { + // Use v3 area size system + using zelda3::AreaSizeEnum; + auto area_size = overworld_->overworld_map(current_map_)->area_size(); + + if (area_size != AreaSizeEnum::SmallArea) { + // Get all sibling maps that need palette updates + std::vector sibling_maps; + int parent_id = overworld_->overworld_map(current_map_)->parent(); + + switch (area_size) { + case AreaSizeEnum::LargeArea: + // 2x2 grid: parent, parent+1, parent+8, parent+9 + sibling_maps = {parent_id, parent_id + 1, parent_id + 8, + parent_id + 9}; + break; + case AreaSizeEnum::WideArea: + // 2x1 grid: parent, parent+1 + sibling_maps = {parent_id, parent_id + 1}; + break; + case AreaSizeEnum::TallArea: + // 1x2 grid: parent, parent+8 + sibling_maps = {parent_id, parent_id + 8}; + break; + default: + break; + } + + // Update palette for all siblings + for (int sibling_index : sibling_maps) { + if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) { + continue; + } + RETURN_IF_ERROR( + overworld_->mutable_overworld_map(sibling_index)->LoadPalette()); + (*maps_bmp_)[sibling_index].SetPalette(current_map_palette); + } + } else { + // Small area - only update current map + (*maps_bmp_)[current_map_].SetPalette(current_map_palette); + } + } else { + // Legacy logic for vanilla and v2 ROMs + if (overworld_->overworld_map(current_map_)->is_large_map()) { + // We need to update the map and its siblings if it's a large map + for (int i = 1; i < 4; i++) { + int sibling_index = + overworld_->overworld_map(current_map_)->parent() + i; + if (i >= 2) + sibling_index += 6; + RETURN_IF_ERROR( + overworld_->mutable_overworld_map(sibling_index)->LoadPalette()); + (*maps_bmp_)[sibling_index].SetPalette(current_map_palette); + } + } + (*maps_bmp_)[current_map_].SetPalette(current_map_palette); + } + + return absl::OkStatus(); +} + +absl::Status OverworldGraphicsManager::RefreshTile16Blockset() { + LOG_DEBUG("OverworldGraphicsManager", "RefreshTile16Blockset called"); + if (current_blockset_ == + overworld_->overworld_map(current_map_)->area_graphics()) { + return absl::OkStatus(); + } + current_blockset_ = overworld_->overworld_map(current_map_)->area_graphics(); + + overworld_->set_current_map(current_map_); + *palette_ = overworld_->current_area_palette(); + + const auto tile16_data = overworld_->tile16_blockset_data(); + + gfx::UpdateTilemap(*tile16_blockset_, tile16_data); + tile16_blockset_->atlas.SetPalette(*palette_); + return absl::OkStatus(); +} + +void OverworldGraphicsManager::ForceRefreshGraphics(int map_index) { + if (map_index < 0 || map_index >= zelda3::kNumOverworldMaps) { + return; + } + + LOG_INFO("OverworldGraphicsManager", + "ForceRefreshGraphics: Forcing graphics reload for map %d", map_index); + + // Mark bitmap as modified to force refresh + (*maps_bmp_)[map_index].set_modified(true); + + // Clear the blockset cache to force tile16 reload + current_blockset_ = 0xFF; + + // If this is the current map, also ensure sibling maps are refreshed for multi-area maps + uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; + bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF); + + auto* map = overworld_->mutable_overworld_map(map_index); + if (use_v3_area_sizes) { + using zelda3::AreaSizeEnum; + auto area_size = map->area_size(); + + if (area_size != AreaSizeEnum::SmallArea) { + std::vector sibling_maps; + int parent_id = map->parent(); + + switch (area_size) { + case AreaSizeEnum::LargeArea: + sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9}; + break; + case AreaSizeEnum::WideArea: + sibling_maps = {parent_id, parent_id + 1}; + break; + case AreaSizeEnum::TallArea: + sibling_maps = {parent_id, parent_id + 8}; + break; + default: + break; + } + + // Mark all sibling maps as needing refresh + for (int sibling : sibling_maps) { + if (sibling >= 0 && sibling < zelda3::kNumOverworldMaps) { + (*maps_bmp_)[sibling].set_modified(true); + } + } + } + } else if (map->is_large_map()) { + // Legacy large map handling + int parent_id = map->parent(); + for (int i = 0; i < 4; ++i) { + int sibling = parent_id + (i < 2 ? i : i + 6); + if (sibling >= 0 && sibling < zelda3::kNumOverworldMaps) { + (*maps_bmp_)[sibling].set_modified(true); + } + } + } +} + +} // namespace editor +} // namespace yaze + diff --git a/src/app/editor/overworld/overworld_graphics_manager.h b/src/app/editor/overworld/overworld_graphics_manager.h new file mode 100644 index 00000000..50a85a5d --- /dev/null +++ b/src/app/editor/overworld/overworld_graphics_manager.h @@ -0,0 +1,204 @@ +#ifndef YAZE_APP_EDITOR_OVERWORLD_OVERWORLD_GRAPHICS_MANAGER_H +#define YAZE_APP_EDITOR_OVERWORLD_OVERWORLD_GRAPHICS_MANAGER_H + +#include +#include +#include + +#include "absl/status/status.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" +#include "app/gfx/tilemap.h" +#include "app/rom.h" +#include "app/zelda3/overworld/overworld.h" +#include "app/zelda3/overworld/overworld_map.h" + +namespace yaze { +namespace editor { + +using Tilemap = gfx::Tilemap; + +/** + * @class OverworldGraphicsManager + * @brief Manages all graphics-related operations for the Overworld Editor + * + * This class handles: + * - Graphics loading and initialization + * - Sprite graphics loading + * - Deferred texture processing for smooth loading + * - Map texture management + * - Map refreshing (full and on-demand) + * - Palette refreshing + * - Tile16 blockset refreshing + * - Multi-area map coordination + * + * Separating graphics management from the main OverworldEditor improves: + * - Code organization and maintainability + * - Performance optimization opportunities + * - Testing and debugging + * - Clear separation of concerns + */ +class OverworldGraphicsManager { + public: + OverworldGraphicsManager( + zelda3::Overworld* overworld, Rom* rom, + std::array* maps_bmp, + gfx::Bitmap* tile16_blockset_bmp, gfx::Bitmap* current_gfx_bmp, + std::vector* sprite_previews, gfx::SnesPalette* palette, + Tilemap* tile16_blockset) + : overworld_(overworld), + rom_(rom), + maps_bmp_(maps_bmp), + tile16_blockset_bmp_(tile16_blockset_bmp), + current_gfx_bmp_(current_gfx_bmp), + sprite_previews_(sprite_previews), + palette_(palette), + tile16_blockset_(tile16_blockset) {} + + // ============================================================================ + // Loading Operations + // ============================================================================ + + /** + * @brief Load all overworld graphics (maps, tilesets, sprites) + * + * This uses a multi-phase loading strategy: + * - Phase 1: Create bitmaps without textures + * - Phase 2: Create bitmaps for essential maps only + * - Phase 3: Create textures for visible maps + * - Deferred loading for remaining maps + */ + absl::Status LoadGraphics(); + + /** + * @brief Load sprite graphics for all overworld maps + */ + absl::Status LoadSpriteGraphics(); + + // ============================================================================ + // Texture Management + // ============================================================================ + + /** + * @brief Process deferred texture creation (called per frame) + * + * Creates textures gradually to avoid frame drops. + * Prioritizes textures for the current world and visible maps. + */ + void ProcessDeferredTextures(); + + /** + * @brief Ensure a specific map has a texture created + * + * @param map_index Index of the map to ensure texture for + */ + void EnsureMapTexture(int map_index); + + // ============================================================================ + // Refresh Operations + // ============================================================================ + + /** + * @brief Refresh the current overworld map + */ + void RefreshOverworldMap(); + + /** + * @brief Refresh a specific map on-demand (only if visible) + * + * @param map_index Index of the map to refresh + */ + void RefreshOverworldMapOnDemand(int map_index); + + /** + * @brief Refresh a child map (legacy method) + * + * @param map_index Index of the map to refresh + */ + void RefreshChildMap(int map_index); + + /** + * @brief Refresh a child map with selective updates + * + * @param map_index Index of the map to refresh + */ + void RefreshChildMapOnDemand(int map_index); + + /** + * @brief Safely refresh multi-area maps (large, wide, tall) + * + * Handles coordination of multi-area maps without recursion. + * + * @param map_index Index of the map to refresh + * @param map Pointer to the OverworldMap object + */ + void RefreshMultiAreaMapsSafely(int map_index, zelda3::OverworldMap* map); + + /** + * @brief Refresh the palette for the current map + * + * Also handles palette updates for multi-area maps. + */ + absl::Status RefreshMapPalette(); + + /** + * @brief Refresh the tile16 blockset + * + * This should be called whenever area graphics change. + */ + absl::Status RefreshTile16Blockset(); + + /** + * @brief Force a graphics refresh for a specific map + * + * Marks the map's bitmap as modified and clears the blockset cache + * to force a full reload on next refresh. Use this when graphics + * properties change (area_graphics, animated_gfx, custom tilesets). + * + * @param map_index Index of the map to force refresh + */ + void ForceRefreshGraphics(int map_index); + + // ============================================================================ + // State Management + // ============================================================================ + + void set_current_map(int map_index) { current_map_ = map_index; } + void set_current_world(int world_index) { current_world_ = world_index; } + void set_current_blockset(uint8_t blockset) { current_blockset_ = blockset; } + + int current_map() const { return current_map_; } + int current_world() const { return current_world_; } + uint8_t current_blockset() const { return current_blockset_; } + + bool all_gfx_loaded() const { return all_gfx_loaded_; } + bool map_blockset_loaded() const { return map_blockset_loaded_; } + + private: + // Core dependencies + zelda3::Overworld* overworld_; + Rom* rom_; + std::array* maps_bmp_; + gfx::Bitmap* tile16_blockset_bmp_; + gfx::Bitmap* current_gfx_bmp_; + std::vector* sprite_previews_; + gfx::SnesPalette* palette_; + Tilemap* tile16_blockset_; + + // State tracking + int current_map_ = 0; + int current_world_ = 0; + uint8_t current_blockset_ = 0; + bool all_gfx_loaded_ = false; + bool map_blockset_loaded_ = false; + + // Deferred texture loading + std::vector deferred_map_textures_; + std::mutex deferred_textures_mutex_; +}; + +} // namespace editor +} // namespace yaze + +#endif // YAZE_APP_EDITOR_OVERWORLD_OVERWORLD_GRAPHICS_MANAGER_H + diff --git a/src/app/zelda3/overworld/overworld.cc b/src/app/zelda3/overworld/overworld.cc index afff5854..324e4561 100644 --- a/src/app/zelda3/overworld/overworld.cc +++ b/src/app/zelda3/overworld/overworld.cc @@ -633,14 +633,6 @@ absl::Status Overworld::LoadExits() { uint16_t px = (uint16_t)((rom_data[OWExitXPlayer + (i * 2) + 1] << 8) + rom_data[OWExitXPlayer + (i * 2)]); - // util::logf( - // "Exit: %d RoomID: %d MapID: %d VRAM: %d YScroll: %d XScroll: " - // "%d YPlayer: %d XPlayer: %d YCamera: %d XCamera: %d " - // "ScrollModY: %d ScrollModX: %d DoorType1: %d DoorType2: %d", - // i, exit_room_id, exit_map_id, exit_vram, exit_y_scroll, exit_x_scroll, - // py, px, exit_y_camera, exit_x_camera, exit_scroll_mod_y, - // exit_scroll_mod_x, exit_door_type_1, exit_door_type_2); - exits.emplace_back(exit_room_id, exit_map_id, exit_vram, exit_y_scroll, exit_x_scroll, py, px, exit_y_camera, exit_x_camera, exit_scroll_mod_y, exit_scroll_mod_x, exit_door_type_1, diff --git a/src/app/zelda3/overworld/overworld_exit.h b/src/app/zelda3/overworld/overworld_exit.h index c26b14e5..22c04cc3 100644 --- a/src/app/zelda3/overworld/overworld_exit.h +++ b/src/app/zelda3/overworld/overworld_exit.h @@ -194,10 +194,6 @@ class OverworldExit : public GameEntity { map_pos_ = (uint16_t)(((vram_y_scroll & 0xFFF0) << 3) | ((vram_x_scroll & 0xFFF0) >> 3)); - - std::cout << "Exit: " << room_id_ << " MapId: " << std::hex << mapid - << " X: " << static_cast(area_x_) - << " Y: " << static_cast(area_y_) << std::endl; } }; diff --git a/src/app/zelda3/overworld/overworld_item.h b/src/app/zelda3/overworld/overworld_item.h index 3a69fb44..c546c8a4 100644 --- a/src/app/zelda3/overworld/overworld_item.h +++ b/src/app/zelda3/overworld/overworld_item.h @@ -53,12 +53,6 @@ class OverworldItem : public GameEntity { game_x_ = static_cast(std::abs(x_ - (map_x * 512)) / 16); game_y_ = static_cast(std::abs(y_ - (map_y * 512)) / 16); - - std::cout << "Item: " << std::hex << std::setw(2) << std::setfill('0') - << static_cast(id_) << " MapId: " << std::hex << std::setw(2) - << std::setfill('0') << static_cast(room_map_id_) - << " X: " << static_cast(game_x_) - << " Y: " << static_cast(game_y_) << std::endl; } bool bg2_ = false;