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.
This commit is contained in:
scawful
2025-10-09 20:56:56 -04:00
parent 51342b02e3
commit 46f078beed
6 changed files with 308 additions and 72 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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<uint8_t> 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 &regions_[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<float>(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

150
src/app/gfx/texture_atlas.h Normal file
View File

@@ -0,0 +1,150 @@
#ifndef YAZE_APP_GFX_TEXTURE_ATLAS_H
#define YAZE_APP_GFX_TEXTURE_ATLAS_H
#include <map>
#include <memory>
#include <vector>
#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<int, AtlasRegion> 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

View File

@@ -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<uint8_t> 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<int>(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()) {

View File

@@ -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();