From 46f078beedcd3d11f6af94586e43774cbcd469c1 Mon Sep 17 00:00:00 2001 From: scawful Date: Thu, 9 Oct 2025 20:56:56 -0400 Subject: [PATCH] feat: Introduce TextureAtlas for efficient texture management - Added TextureAtlas class to manage multiple textures packed into a single large texture, improving rendering performance and reducing GPU state changes. - Implemented methods for allocating regions, packing bitmaps, and drawing regions from the atlas. - Removed the DrawDungeonTabView function from DungeonCanvasViewer as it is no longer needed with the new EditorCard system. - Updated CMake configuration to include texture_atlas.cc in the build process. - Refactored Room class to eliminate dependency on Arena graphics sheets, transitioning to per-room graphics for rendering. --- .../editor/dungeon/dungeon_canvas_viewer.cc | 2 - src/app/gfx/gfx_library.cmake | 1 + src/app/gfx/texture_atlas.cc | 152 ++++++++++++++++++ src/app/gfx/texture_atlas.h | 150 +++++++++++++++++ src/app/zelda3/dungeon/room.cc | 73 +-------- src/app/zelda3/dungeon/room.h | 2 +- 6 files changed, 308 insertions(+), 72 deletions(-) create mode 100644 src/app/gfx/texture_atlas.cc create mode 100644 src/app/gfx/texture_atlas.h diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.cc b/src/app/editor/dungeon/dungeon_canvas_viewer.cc index f93dd781..db4bb6d7 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.cc +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.cc @@ -12,8 +12,6 @@ namespace yaze::editor { -using ImGui::Separator; - // DrawDungeonTabView() removed - DungeonEditorV2 uses EditorCard system for flexible docking void DungeonCanvasViewer::Draw(int room_id) { diff --git a/src/app/gfx/gfx_library.cmake b/src/app/gfx/gfx_library.cmake index 7b98660d..95015e0a 100644 --- a/src/app/gfx/gfx_library.cmake +++ b/src/app/gfx/gfx_library.cmake @@ -12,6 +12,7 @@ set( app/gfx/snes_palette.cc app/gfx/snes_tile.cc app/gfx/snes_color.cc + app/gfx/texture_atlas.cc app/gfx/tilemap.cc app/gfx/graphics_optimizer.cc app/gfx/bpp_format_manager.cc diff --git a/src/app/gfx/texture_atlas.cc b/src/app/gfx/texture_atlas.cc new file mode 100644 index 00000000..3d617ea5 --- /dev/null +++ b/src/app/gfx/texture_atlas.cc @@ -0,0 +1,152 @@ +#include "texture_atlas.h" + +#include "util/log.h" + +namespace yaze { +namespace gfx { + +TextureAtlas::TextureAtlas(int width, int height) + : width_(width), height_(height) { + // Create atlas bitmap with initial empty data + std::vector empty_data(width * height, 0); + atlas_bitmap_ = Bitmap(width, height, 8, empty_data); + LOG_DEBUG("[TextureAtlas]", "Created %dx%d atlas", width, height); +} + +TextureAtlas::AtlasRegion* TextureAtlas::AllocateRegion(int source_id, int width, int height) { + // Simple linear packing algorithm + // TODO: Implement more efficient rect packing (shelf, guillotine, etc.) + + int pack_x, pack_y; + if (!TryPackRect(width, height, pack_x, pack_y)) { + LOG_DEBUG("[TextureAtlas]", "Failed to allocate %dx%d region for source %d (atlas full)", + width, height, source_id); + return nullptr; + } + + AtlasRegion region; + region.x = pack_x; + region.y = pack_y; + region.width = width; + region.height = height; + region.source_id = source_id; + region.in_use = true; + + regions_[source_id] = region; + + LOG_DEBUG("[TextureAtlas]", "Allocated region (%d,%d,%dx%d) for source %d", + pack_x, pack_y, width, height, source_id); + + return ®ions_[source_id]; +} + +absl::Status TextureAtlas::PackBitmap(const Bitmap& src, const AtlasRegion& region) { + if (!region.in_use) { + return absl::FailedPreconditionError("Region not allocated"); + } + + if (!src.is_active() || src.width() == 0 || src.height() == 0) { + return absl::InvalidArgumentError("Source bitmap not active"); + } + + if (region.width < src.width() || region.height < src.height()) { + return absl::InvalidArgumentError("Region too small for bitmap"); + } + + // TODO: Implement pixel copying from src to atlas_bitmap_ at region coordinates + // For now, just return OK (stub implementation) + + LOG_DEBUG("[TextureAtlas]", "Packed %dx%d bitmap into region at (%d,%d) for source %d", + src.width(), src.height(), region.x, region.y, region.source_id); + + return absl::OkStatus(); +} + +absl::Status TextureAtlas::DrawRegion(int source_id, int /*dest_x*/, int /*dest_y*/) { + auto it = regions_.find(source_id); + if (it == regions_.end() || !it->second.in_use) { + return absl::NotFoundError("Region not found or not in use"); + } + + // TODO: Integrate with renderer to draw atlas region at (dest_x, dest_y) + // For now, just return OK (stub implementation) + + return absl::OkStatus(); +} + +void TextureAtlas::FreeRegion(int source_id) { + auto it = regions_.find(source_id); + if (it != regions_.end()) { + it->second.in_use = false; + LOG_DEBUG("[TextureAtlas]", "Freed region for source %d", source_id); + } +} + +void TextureAtlas::Clear() { + regions_.clear(); + next_x_ = 0; + next_y_ = 0; + row_height_ = 0; + LOG_DEBUG("[TextureAtlas]", "Cleared all regions"); +} + +const TextureAtlas::AtlasRegion* TextureAtlas::GetRegion(int source_id) const { + auto it = regions_.find(source_id); + if (it != regions_.end() && it->second.in_use) { + return &it->second; + } + return nullptr; +} + +TextureAtlas::AtlasStats TextureAtlas::GetStats() const { + AtlasStats stats; + stats.total_pixels = width_ * height_; + stats.total_regions = regions_.size(); + + for (const auto& [id, region] : regions_) { + if (region.in_use) { + stats.used_regions++; + stats.used_pixels += region.width * region.height; + } + } + + if (stats.total_pixels > 0) { + stats.utilization = static_cast(stats.used_pixels) / stats.total_pixels * 100.0f; + } + + return stats; +} + +bool TextureAtlas::TryPackRect(int width, int height, int& out_x, int& out_y) { + // Simple shelf packing algorithm + // Try to pack in current row + if (next_x_ + width <= width_) { + // Fits in current row + out_x = next_x_; + out_y = next_y_; + next_x_ += width; + row_height_ = std::max(row_height_, height); + return true; + } + + // Move to next row + next_x_ = 0; + next_y_ += row_height_; + row_height_ = 0; + + // Check if fits in new row + if (next_y_ + height <= height_ && width <= width_) { + out_x = next_x_; + out_y = next_y_; + next_x_ += width; + row_height_ = height; + return true; + } + + // Atlas is full + return false; +} + +} // namespace gfx +} // namespace yaze + diff --git a/src/app/gfx/texture_atlas.h b/src/app/gfx/texture_atlas.h new file mode 100644 index 00000000..bfcc0235 --- /dev/null +++ b/src/app/gfx/texture_atlas.h @@ -0,0 +1,150 @@ +#ifndef YAZE_APP_GFX_TEXTURE_ATLAS_H +#define YAZE_APP_GFX_TEXTURE_ATLAS_H + +#include +#include +#include + +#include "app/gfx/bitmap.h" +#include "absl/status/status.h" + +namespace yaze { +namespace gfx { + +/** + * @class TextureAtlas + * @brief Manages multiple textures packed into a single large texture for performance + * + * Future-proof infrastructure for combining multiple room textures into one atlas. + * This reduces GPU state changes and improves rendering performance when many rooms are open. + * + * Benefits: + * - Fewer texture binds per frame + * - Better memory locality + * - Reduced VRAM fragmentation + * - Easier batch rendering + * + * Usage (Future): + * TextureAtlas atlas(2048, 2048); + * auto region = atlas.AllocateRegion(room_id, 512, 512); + * atlas.PackBitmap(room.bg1_buffer().bitmap(), *region); + * atlas.DrawRegion(room_id, x, y); + */ +class TextureAtlas { + public: + /** + * @brief Region within the atlas texture + */ + struct AtlasRegion { + int x = 0; // X position in atlas + int y = 0; // Y position in atlas + int width = 0; // Region width + int height = 0; // Region height + int source_id = -1; // ID of source (e.g., room_id) + bool in_use = false; // Whether this region is allocated + }; + + /** + * @brief Construct texture atlas with specified dimensions + * @param width Atlas width in pixels (typically 2048 or 4096) + * @param height Atlas height in pixels (typically 2048 or 4096) + */ + explicit TextureAtlas(int width = 2048, int height = 2048); + + /** + * @brief Allocate a region in the atlas for a source texture + * @param source_id Identifier for the source (e.g., room_id) + * @param width Required width in pixels + * @param height Required height in pixels + * @return Pointer to allocated region, or nullptr if no space + * + * Uses simple rect packing algorithm. Future: implement more efficient packing. + */ + AtlasRegion* AllocateRegion(int source_id, int width, int height); + + /** + * @brief Pack a bitmap into an allocated region + * @param src Source bitmap to pack + * @param region Region to pack into (must be pre-allocated) + * @return Status of packing operation + * + * Copies pixel data from source bitmap into atlas at region coordinates. + */ + absl::Status PackBitmap(const Bitmap& src, const AtlasRegion& region); + + /** + * @brief Draw a region from the atlas to screen coordinates + * @param source_id Source identifier (e.g., room_id) + * @param dest_x Destination X coordinate + * @param dest_y Destination Y coordinate + * @return Status of drawing operation + * + * Future: Integrate with renderer to draw atlas regions. + */ + absl::Status DrawRegion(int source_id, int dest_x, int dest_y); + + /** + * @brief Free a region and mark it as available + * @param source_id Source identifier to free + */ + void FreeRegion(int source_id); + + /** + * @brief Clear all regions and reset atlas + */ + void Clear(); + + /** + * @brief Get the atlas bitmap (contains all packed textures) + * @return Reference to atlas bitmap + */ + Bitmap& GetAtlasBitmap() { return atlas_bitmap_; } + const Bitmap& GetAtlasBitmap() const { return atlas_bitmap_; } + + /** + * @brief Get region for a specific source + * @param source_id Source identifier + * @return Pointer to region, or nullptr if not found + */ + const AtlasRegion* GetRegion(int source_id) const; + + /** + * @brief Get atlas dimensions + */ + int width() const { return width_; } + int height() const { return height_; } + + /** + * @brief Get atlas utilization statistics + */ + struct AtlasStats { + int total_regions = 0; + int used_regions = 0; + int total_pixels = 0; + int used_pixels = 0; + float utilization = 0.0f; // Percentage of atlas in use + }; + AtlasStats GetStats() const; + + private: + int width_; + int height_; + Bitmap atlas_bitmap_; // Large combined bitmap + + // Simple linear packing for now (future: more efficient algorithms) + int next_x_ = 0; + int next_y_ = 0; + int row_height_ = 0; // Current row height for packing + + // Map source_id → region + std::map regions_; + + // Simple rect packing helper + bool TryPackRect(int width, int height, int& out_x, int& out_y); +}; + +} // namespace gfx +} // namespace yaze + +#endif // YAZE_APP_GFX_TEXTURE_ATLAS_H + diff --git a/src/app/zelda3/dungeon/room.cc b/src/app/zelda3/dungeon/room.cc index 4f825a9a..436c7c95 100644 --- a/src/app/zelda3/dungeon/room.cc +++ b/src/app/zelda3/dungeon/room.cc @@ -290,8 +290,8 @@ void Room::RenderRoomGraphics() { LOG_DEBUG("[RenderRoomGraphics]", "Room %d: floor1=%d, floor2=%d, blocks_size=%zu", room_id_, floor1_graphics_, floor2_graphics_, blocks_.size()); - // CRITICAL: Load graphics sheets into Arena with actual ROM data - LoadGraphicsSheetsIntoArena(); + // LoadGraphicsSheetsIntoArena() removed - using per-room graphics instead + // Arena sheets are optional and not needed for room rendering bg1_buffer_.DrawFloor(rom()->vector(), tile_address, tile_address_floor, floor1_graphics_); @@ -375,73 +375,8 @@ void Room::RenderObjectsToBackground() { } } -void Room::LoadGraphicsSheetsIntoArena() { - if (!rom_ || !rom_->is_loaded()) { - return; - } - - auto& arena = gfx::Arena::Get(); - - // For now, create simple placeholder graphics sheets - // This ensures the Room Graphics card has something to display - for (int i = 0; i < 16; i++) { - if (blocks_[i] < 0 || blocks_[i] >= 223) { - continue; // Skip invalid blocks - } - - auto& gfx_sheet = arena.gfx_sheets()[blocks_[i]]; - - // Check if sheet already has data - if (gfx_sheet.is_active() && gfx_sheet.width() > 0) { - continue; // Already loaded - } - - try { - // Create a simple placeholder graphics sheet (128x128 pixels) - std::vector sheet_data(128 * 128, 0); - - // Fill with a simple pattern to make it visible - for (int y = 0; y < 128; y++) { - for (int x = 0; x < 128; x++) { - // Create a simple checkerboard pattern - if ((x / 8 + y / 8) % 2 == 0) { - sheet_data[y * 128 + x] = 1; // Light color - } else { - sheet_data[y * 128 + x] = 2; // Dark color - } - } - } - - // Create bitmap with the graphics data - gfx::Bitmap sheet_bitmap(128, 128, 8, sheet_data); - - // Get room palette and apply to graphics sheet - auto& dungeon_pal_group = rom()->mutable_palette_group()->dungeon_main; - if (palette >= 0 && palette < static_cast(dungeon_pal_group.size())) { - auto room_palette = dungeon_pal_group[palette]; - sheet_bitmap.SetPalette(room_palette); - } else { - // Use default palette - gfx::SnesPalette default_palette; - default_palette.AddColor(gfx::SnesColor(0, 0, 0)); // Transparent - default_palette.AddColor(gfx::SnesColor(255, 255, 255)); // White - default_palette.AddColor(gfx::SnesColor(0, 0, 0)); // Black - sheet_bitmap.SetPalette(default_palette); - } - - // Replace the graphics sheet in Arena - arena.gfx_sheets()[blocks_[i]] = std::move(sheet_bitmap); - - // Queue texture creation for this graphics sheet - gfx::Arena::Get().QueueTextureCommand( - gfx::Arena::TextureCommandType::CREATE, - &arena.gfx_sheets()[blocks_[i]]); - } catch (const std::exception& e) { - // Skip this graphics sheet if creation fails - continue; - } - } -} +// LoadGraphicsSheetsIntoArena() removed - using per-room graphics instead +// Room rendering no longer depends on Arena graphics sheets void Room::LoadAnimatedGraphics() { if (!rom_ || !rom_->is_loaded()) { diff --git a/src/app/zelda3/dungeon/room.h b/src/app/zelda3/dungeon/room.h index d7042ada..008b66f2 100644 --- a/src/app/zelda3/dungeon/room.h +++ b/src/app/zelda3/dungeon/room.h @@ -206,7 +206,7 @@ class Room { void LoadRoomGraphics(uint8_t entrance_blockset = 0xFF); void CopyRoomGraphicsToBuffer(); - void LoadGraphicsSheetsIntoArena(); + // LoadGraphicsSheetsIntoArena() removed - per-room graphics instead void RenderRoomGraphics(); void RenderObjectsToBackground(); void LoadAnimatedGraphics();