refactor(gfx): reorganize graphics includes and introduce new types

- Updated include paths for various graphics-related headers to improve organization and clarity.
- Introduced new types for SNES color, palette, and tile management, enhancing the structure of the graphics subsystem.
- Refactored existing code to utilize the new types, ensuring consistency across the codebase.

Benefits:
- Improves maintainability and readability of the graphics code.
- Facilitates future enhancements and optimizations within the graphics subsystem.
This commit is contained in:
scawful
2025-10-13 00:09:34 -04:00
parent c0d410d7f0
commit 6374da6194
131 changed files with 429 additions and 315 deletions

View File

@@ -0,0 +1,454 @@
#include "app/gfx/render/atlas_renderer.h"
#include <algorithm>
#include <cmath>
#include "app/gfx/util/bpp_format_manager.h"
namespace yaze {
namespace gfx {
AtlasRenderer& AtlasRenderer::Get() {
static AtlasRenderer instance;
return instance;
}
void AtlasRenderer::Initialize(IRenderer* renderer, int initial_size) {
renderer_ = renderer;
next_atlas_id_ = 0;
current_atlas_ = 0;
// Clear any existing atlases
Clear();
// Create initial atlas
CreateNewAtlas();
}
int AtlasRenderer::AddBitmap(const Bitmap& bitmap) {
if (!bitmap.is_active() || !bitmap.texture()) {
return -1; // Invalid bitmap
}
ScopedTimer timer("atlas_add_bitmap");
// Try to pack into current atlas
SDL_Rect uv_rect;
if (PackBitmap(*atlases_[current_atlas_], bitmap, uv_rect)) {
int atlas_id = next_atlas_id_++;
auto& atlas = *atlases_[current_atlas_];
// Copy bitmap data to atlas texture
renderer_->SetRenderTarget(atlas.texture);
renderer_->RenderCopy(bitmap.texture(), nullptr, &uv_rect);
renderer_->SetRenderTarget(nullptr);
return atlas_id;
}
// Current atlas is full, create new one
CreateNewAtlas();
if (PackBitmap(*atlases_[current_atlas_], bitmap, uv_rect)) {
int atlas_id = next_atlas_id_++;
auto& atlas = *atlases_[current_atlas_];
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
renderer_->SetRenderTarget(atlas.texture);
renderer_->RenderCopy(bitmap.texture(), nullptr, &uv_rect);
renderer_->SetRenderTarget(nullptr);
return atlas_id;
}
return -1; // Failed to add
}
int AtlasRenderer::AddBitmapWithBppOptimization(const Bitmap& bitmap, BppFormat target_bpp) {
if (!bitmap.is_active() || !bitmap.texture()) {
return -1; // Invalid bitmap
}
ScopedTimer timer("atlas_add_bitmap_bpp_optimized");
// Detect current BPP format
BppFormat current_bpp = BppFormatManager::Get().DetectFormat(bitmap.vector(), bitmap.width(), bitmap.height());
// If formats match, use standard addition
if (current_bpp == target_bpp) {
return AddBitmap(bitmap);
}
// Convert bitmap to target BPP format
auto converted_data = BppFormatManager::Get().ConvertFormat(
bitmap.vector(), current_bpp, target_bpp, bitmap.width(), bitmap.height());
// Create temporary bitmap with converted data
Bitmap converted_bitmap(bitmap.width(), bitmap.height(), bitmap.depth(), converted_data, bitmap.palette());
converted_bitmap.CreateTexture();
// Add converted bitmap to atlas
return AddBitmap(converted_bitmap);
}
void AtlasRenderer::RemoveBitmap(int atlas_id) {
auto it = atlas_lookup_.find(atlas_id);
if (it == atlas_lookup_.end()) {
return;
}
AtlasEntry* entry = it->second;
entry->in_use = false;
// Mark region as free
for (auto& atlas : atlases_) {
for (auto& atlas_entry : atlas->entries) {
if (atlas_entry.atlas_id == atlas_id) {
MarkRegionUsed(*atlas, atlas_entry.uv_rect, false);
break;
}
}
}
atlas_lookup_.erase(it);
}
void AtlasRenderer::UpdateBitmap(int atlas_id, const Bitmap& bitmap) {
auto it = atlas_lookup_.find(atlas_id);
if (it == atlas_lookup_.end()) {
return;
}
AtlasEntry* entry = it->second;
entry->texture = bitmap.texture();
// Update UV coordinates if size changed
if (bitmap.width() != entry->uv_rect.w || bitmap.height() != entry->uv_rect.h) {
// Remove old entry and add new one
RemoveBitmap(atlas_id);
AddBitmap(bitmap);
}
}
void AtlasRenderer::RenderBatch(const std::vector<RenderCommand>& render_commands) {
if (render_commands.empty()) {
return;
}
ScopedTimer timer("atlas_batch_render");
// Group commands by atlas for efficient rendering
std::unordered_map<int, std::vector<const RenderCommand*>> atlas_groups;
for (const auto& cmd : render_commands) {
auto it = atlas_lookup_.find(cmd.atlas_id);
if (it != atlas_lookup_.end() && it->second->in_use) {
// Find which atlas contains this entry
for (size_t i = 0; i < atlases_.size(); ++i) {
for (const auto& entry : atlases_[i]->entries) {
if (entry.atlas_id == cmd.atlas_id) {
atlas_groups[i].push_back(&cmd);
break;
}
}
}
}
}
// Render each atlas group
for (const auto& [atlas_index, commands] : atlas_groups) {
if (commands.empty()) continue;
auto& atlas = *atlases_[atlas_index];
// Set atlas texture
// SDL_SetTextureBlendMode(atlas.texture, SDL_BLENDMODE_BLEND);
// Render all commands for this atlas
for (const auto* cmd : commands) {
auto it = atlas_lookup_.find(cmd->atlas_id);
if (it == atlas_lookup_.end()) continue;
AtlasEntry* entry = it->second;
// Calculate destination rectangle
SDL_Rect dest_rect = {
static_cast<int>(cmd->x),
static_cast<int>(cmd->y),
static_cast<int>(entry->uv_rect.w * cmd->scale_x),
static_cast<int>(entry->uv_rect.h * cmd->scale_y)
};
// Apply rotation if needed
if (std::abs(cmd->rotation) > 0.001F) {
// For rotation, we'd need to use SDL_RenderCopyEx
// This is a simplified version
renderer_->RenderCopy(atlas.texture, &entry->uv_rect, &dest_rect);
} else {
renderer_->RenderCopy(atlas.texture, &entry->uv_rect, &dest_rect);
}
}
}
}
void AtlasRenderer::RenderBatchWithBppOptimization(const std::vector<RenderCommand>& render_commands,
const std::unordered_map<BppFormat, std::vector<int>>& bpp_groups) {
if (render_commands.empty()) {
return;
}
ScopedTimer timer("atlas_batch_render_bpp_optimized");
// Render each BPP group separately for optimal performance
for (const auto& [bpp_format, command_indices] : bpp_groups) {
if (command_indices.empty()) continue;
// Group commands by atlas for this BPP format
std::unordered_map<int, std::vector<const RenderCommand*>> atlas_groups;
for (int cmd_index : command_indices) {
if (cmd_index >= 0 && cmd_index < static_cast<int>(render_commands.size())) {
const auto& cmd = render_commands[cmd_index];
auto it = atlas_lookup_.find(cmd.atlas_id);
if (it != atlas_lookup_.end() && it->second->in_use && it->second->bpp_format == bpp_format) {
// Find which atlas contains this entry
for (size_t i = 0; i < atlases_.size(); ++i) {
for (const auto& entry : atlases_[i]->entries) {
if (entry.atlas_id == cmd.atlas_id) {
atlas_groups[i].push_back(&cmd);
break;
}
}
}
}
}
}
// Render each atlas group for this BPP format
for (const auto& [atlas_index, commands] : atlas_groups) {
if (commands.empty()) continue;
auto& atlas = *atlases_[atlas_index];
// Set atlas texture with BPP-specific blend mode
// SDL_SetTextureBlendMode(atlas.texture, SDL_BLENDMODE_BLEND);
// Render all commands for this atlas and BPP format
for (const auto* cmd : commands) {
auto it = atlas_lookup_.find(cmd->atlas_id);
if (it == atlas_lookup_.end()) continue;
AtlasEntry* entry = it->second;
// Calculate destination rectangle
SDL_Rect dest_rect = {
static_cast<int>(cmd->x),
static_cast<int>(cmd->y),
static_cast<int>(entry->uv_rect.w * cmd->scale_x),
static_cast<int>(entry->uv_rect.h * cmd->scale_y)
};
// Apply rotation if needed
if (std::abs(cmd->rotation) > 0.001F) {
renderer_->RenderCopy(atlas.texture, &entry->uv_rect, &dest_rect);
} else {
renderer_->RenderCopy(atlas.texture, &entry->uv_rect, &dest_rect);
}
}
}
}
}
AtlasStats AtlasRenderer::GetStats() const {
AtlasStats stats;
stats.total_atlases = atlases_.size();
for (const auto& atlas : atlases_) {
stats.total_entries += atlas->entries.size();
stats.used_entries += std::count_if(atlas->entries.begin(), atlas->entries.end(),
[](const AtlasEntry& entry) { return entry.in_use; });
// Calculate memory usage (simplified)
stats.total_memory += atlas->size * atlas->size * 4; // RGBA8888
}
if (stats.total_entries > 0) {
stats.utilization_percent = (static_cast<float>(stats.used_entries) / stats.total_entries) * 100.0F;
}
return stats;
}
void AtlasRenderer::Defragment() {
ScopedTimer timer("atlas_defragment");
for (auto& atlas : atlases_) {
// Remove unused entries
atlas->entries.erase(
std::remove_if(atlas->entries.begin(), atlas->entries.end(),
[](const AtlasEntry& entry) { return !entry.in_use; }),
atlas->entries.end());
// Rebuild atlas texture
RebuildAtlas(*atlas);
}
}
void AtlasRenderer::Clear() {
// Clean up SDL textures
for (auto& atlas : atlases_) {
if (atlas->texture) {
renderer_->DestroyTexture(atlas->texture);
}
}
atlases_.clear();
atlas_lookup_.clear();
next_atlas_id_ = 0;
current_atlas_ = 0;
}
AtlasRenderer::~AtlasRenderer() {
Clear();
}
void AtlasRenderer::RenderBitmap(int atlas_id, float x, float y, float scale_x, float scale_y) {
auto it = atlas_lookup_.find(atlas_id);
if (it == atlas_lookup_.end() || !it->second->in_use) {
return;
}
AtlasEntry* entry = it->second;
// Find which atlas contains this entry
for (auto& atlas : atlases_) {
for (const auto& atlas_entry : atlas->entries) {
if (atlas_entry.atlas_id == atlas_id) {
// Calculate destination rectangle
SDL_Rect dest_rect = {
static_cast<int>(x),
static_cast<int>(y),
static_cast<int>(entry->uv_rect.w * scale_x),
static_cast<int>(entry->uv_rect.h * scale_y)
};
// Render using atlas texture
// SDL_SetTextureBlendMode(atlas->texture, SDL_BLENDMODE_BLEND);
renderer_->RenderCopy(atlas->texture, &entry->uv_rect, &dest_rect);
return;
}
}
}
}
SDL_Rect AtlasRenderer::GetUVCoordinates(int atlas_id) const {
auto it = atlas_lookup_.find(atlas_id);
if (it == atlas_lookup_.end() || !it->second->in_use) {
return {0, 0, 0, 0};
}
return it->second->uv_rect;
}
bool AtlasRenderer::PackBitmap(Atlas& atlas, const Bitmap& bitmap, SDL_Rect& uv_rect) {
int width = bitmap.width();
int height = bitmap.height();
// Find free region
SDL_Rect free_rect = FindFreeRegion(atlas, width, height);
if (free_rect.w == 0 || free_rect.h == 0) {
return false; // No space available
}
// Mark region as used
MarkRegionUsed(atlas, free_rect, true);
// Set UV coordinates (normalized to 0-1 range)
uv_rect = {
free_rect.x,
free_rect.y,
width,
height
};
return true;
}
void AtlasRenderer::CreateNewAtlas() {
int size = 1024; // Default size
if (!atlases_.empty()) {
size = atlases_.back()->size * 2; // Double size for new atlas
}
atlases_.push_back(std::make_unique<Atlas>(size));
current_atlas_ = atlases_.size() - 1;
// Create SDL texture for the atlas
auto& atlas = *atlases_[current_atlas_];
atlas.texture = renderer_->CreateTexture(size, size);
if (!atlas.texture) {
SDL_Log("Failed to create atlas texture: %s", SDL_GetError());
}
}
void AtlasRenderer::RebuildAtlas(Atlas& atlas) {
// Clear used regions
std::fill(atlas.used_regions.begin(), atlas.used_regions.end(), false);
// Rebuild atlas texture by copying from source textures
renderer_->SetRenderTarget(atlas.texture);
renderer_->SetDrawColor({0, 0, 0, 0});
renderer_->Clear();
for (auto& entry : atlas.entries) {
if (entry.in_use && entry.texture) {
renderer_->RenderCopy(entry.texture, nullptr, &entry.uv_rect);
MarkRegionUsed(atlas, entry.uv_rect, true);
}
}
renderer_->SetRenderTarget(nullptr);
}
SDL_Rect AtlasRenderer::FindFreeRegion(Atlas& atlas, int width, int height) {
// Simple first-fit algorithm
for (int y = 0; y <= atlas.size - height; ++y) {
for (int x = 0; x <= atlas.size - width; ++x) {
bool can_fit = true;
// Check if region is free
for (int dy = 0; dy < height && can_fit; ++dy) {
for (int dx = 0; dx < width && can_fit; ++dx) {
int index = (y + dy) * atlas.size + (x + dx);
if (index >= static_cast<int>(atlas.used_regions.size()) || atlas.used_regions[index]) {
can_fit = false;
}
}
}
if (can_fit) {
return {x, y, width, height};
}
}
}
return {0, 0, 0, 0}; // No space found
}
void AtlasRenderer::MarkRegionUsed(Atlas& atlas, const SDL_Rect& rect, bool used) {
for (int y = rect.y; y < rect.y + rect.h; ++y) {
for (int x = rect.x; x < rect.x + rect.w; ++x) {
int index = y * atlas.size + x;
if (index >= 0 && index < static_cast<int>(atlas.used_regions.size())) {
atlas.used_regions[index] = used;
}
}
}
}
} // namespace gfx
} // namespace yaze

View File

@@ -0,0 +1,206 @@
#ifndef YAZE_APP_GFX_ATLAS_RENDERER_H
#define YAZE_APP_GFX_ATLAS_RENDERER_H
#include <SDL.h>
#include <vector>
#include <unordered_map>
#include <memory>
#include "app/gfx/core/bitmap.h"
#include "app/gfx/debug/performance/performance_profiler.h"
#include "app/gfx/util/bpp_format_manager.h"
namespace yaze {
namespace gfx {
/**
* @brief Render command for batch rendering
*/
struct RenderCommand {
int atlas_id; ///< Atlas ID of bitmap to render
float x, y; ///< Screen coordinates
float scale_x, scale_y; ///< Scale factors
float rotation; ///< Rotation angle in degrees
SDL_Color tint; ///< Color tint
RenderCommand(int id, float x_pos, float y_pos,
float sx = 1.0f, float sy = 1.0f,
float rot = 0.0f, SDL_Color color = {255, 255, 255, 255})
: atlas_id(id), x(x_pos), y(y_pos),
scale_x(sx), scale_y(sy), rotation(rot), tint(color) {}
};
/**
* @brief Atlas usage statistics
*/
struct AtlasStats {
int total_atlases;
int total_entries;
int used_entries;
size_t total_memory;
size_t used_memory;
float utilization_percent;
AtlasStats() : total_atlases(0), total_entries(0), used_entries(0),
total_memory(0), used_memory(0), utilization_percent(0.0f) {}
};
/**
* @brief Atlas-based rendering system for efficient graphics operations
*
* The AtlasRenderer class provides efficient rendering by combining multiple
* graphics elements into a single texture atlas, reducing draw calls and
* improving performance for ROM hacking workflows.
*
* Key Features:
* - Single draw call for multiple tiles/graphics
* - Automatic atlas management and packing
* - Dynamic atlas resizing and reorganization
* - UV coordinate mapping for efficient rendering
* - Memory-efficient texture management
*
* Performance Optimizations:
* - Reduces draw calls from N to 1 for multiple elements
* - Minimizes GPU state changes
* - Efficient texture packing algorithm
* - Automatic atlas defragmentation
*
* ROM Hacking Specific:
* - Optimized for SNES tile rendering (8x8, 16x16)
* - Support for graphics sheet atlasing
* - Efficient palette management across atlas
* - Tile-based UV coordinate system
*/
class AtlasRenderer {
public:
static AtlasRenderer& Get();
/**
* @brief Initialize the atlas renderer
* @param renderer The renderer to use for texture operations
* @param initial_size Initial atlas size (power of 2 recommended)
*/
void Initialize(IRenderer* renderer, int initial_size = 1024);
/**
* @brief Add a bitmap to the atlas
* @param bitmap Bitmap to add to atlas
* @return Atlas ID for referencing this bitmap
*/
int AddBitmap(const Bitmap& bitmap);
/**
* @brief Add a bitmap to the atlas with BPP format optimization
* @param bitmap Bitmap to add to atlas
* @param target_bpp Target BPP format for optimization
* @return Atlas ID for referencing this bitmap
*/
int AddBitmapWithBppOptimization(const Bitmap& bitmap, BppFormat target_bpp);
/**
* @brief Remove a bitmap from the atlas
* @param atlas_id Atlas ID of bitmap to remove
*/
void RemoveBitmap(int atlas_id);
/**
* @brief Update a bitmap in the atlas
* @param atlas_id Atlas ID of bitmap to update
* @param bitmap New bitmap data
*/
void UpdateBitmap(int atlas_id, const Bitmap& bitmap);
/**
* @brief Render multiple bitmaps in a single draw call
* @param render_commands Vector of render commands (atlas_id, x, y, scale)
*/
void RenderBatch(const std::vector<RenderCommand>& render_commands);
/**
* @brief Render multiple bitmaps with BPP-aware batching
* @param render_commands Vector of render commands
* @param bpp_groups Map of BPP format to command groups for optimization
*/
void RenderBatchWithBppOptimization(const std::vector<RenderCommand>& render_commands,
const std::unordered_map<BppFormat, std::vector<int>>& bpp_groups);
/**
* @brief Get atlas statistics
* @return Atlas usage statistics
*/
AtlasStats GetStats() const;
/**
* @brief Defragment the atlas to reclaim space
*/
void Defragment();
/**
* @brief Clear all atlases
*/
void Clear();
/**
* @brief Render a single bitmap using atlas (convenience method)
* @param atlas_id Atlas ID of bitmap to render
* @param x X position on screen
* @param y Y position on screen
* @param scale_x Horizontal scale factor
* @param scale_y Vertical scale factor
*/
void RenderBitmap(int atlas_id, float x, float y, float scale_x = 1.0f, float scale_y = 1.0f);
/**
* @brief Get UV coordinates for a bitmap in the atlas
* @param atlas_id Atlas ID of bitmap
* @return UV rectangle (0-1 normalized coordinates)
*/
SDL_Rect GetUVCoordinates(int atlas_id) const;
private:
AtlasRenderer() = default;
~AtlasRenderer();
struct AtlasEntry {
int atlas_id;
SDL_Rect uv_rect; // UV coordinates in atlas
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, 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 {
TextureHandle texture;
int size;
std::vector<AtlasEntry> entries;
std::vector<bool> used_regions; // Track used regions for packing
Atlas(int s) : size(s), used_regions(s * s, false) {}
};
IRenderer* renderer_;
std::vector<std::unique_ptr<Atlas>> atlases_;
std::unordered_map<int, AtlasEntry*> atlas_lookup_;
int next_atlas_id_;
int current_atlas_;
// Helper methods
bool PackBitmap(Atlas& atlas, const Bitmap& bitmap, SDL_Rect& uv_rect);
void CreateNewAtlas();
void RebuildAtlas(Atlas& atlas);
SDL_Rect FindFreeRegion(Atlas& atlas, int width, int height);
void MarkRegionUsed(Atlas& atlas, const SDL_Rect& rect, bool used);
};
} // namespace gfx
} // namespace yaze
#endif // YAZE_APP_GFX_ATLAS_RENDERER_H

View File

@@ -0,0 +1,218 @@
#include "app/gfx/render/background_buffer.h"
#include <algorithm>
#include <cstdint>
#include <vector>
#include "app/gfx/core/bitmap.h"
#include "app/gfx/types/snes_tile.h"
#include "util/log.h"
namespace yaze::gfx {
BackgroundBuffer::BackgroundBuffer(int width, int height)
: width_(width), height_(height) {
// Initialize buffer with size for SNES layers
const int total_tiles = (width / 8) * (height / 8);
buffer_.resize(total_tiles, 0);
}
void BackgroundBuffer::SetTileAt(int x, int y, uint16_t value) {
if (x < 0 || y < 0) return;
int tiles_w = width_ / 8;
int tiles_h = height_ / 8;
if (x >= tiles_w || y >= tiles_h) return;
buffer_[y * tiles_w + x] = value;
}
uint16_t BackgroundBuffer::GetTileAt(int x, int y) const {
int tiles_w = width_ / 8;
int tiles_h = height_ / 8;
if (x < 0 || y < 0 || x >= tiles_w || y >= tiles_h) return 0;
return buffer_[y * tiles_w + x];
}
void BackgroundBuffer::ClearBuffer() { std::ranges::fill(buffer_, 0); }
void BackgroundBuffer::DrawTile(const TileInfo& tile, uint8_t* canvas,
const uint8_t* tiledata, int indexoffset) {
// tiledata is a 128-pixel-wide indexed bitmap (16 tiles/row * 8 pixels/tile)
// Calculate tile position in the tilesheet
int tile_x = (tile.id_ % 16) * 8; // 16 tiles per row, 8 pixels per tile
int tile_y = (tile.id_ / 16) * 8; // Each row is 16 tiles
// DEBUG: For floor tiles, check what we're actually reading
static int debug_count = 0;
if (debug_count < 4 && (tile.id_ == 0xEC || tile.id_ == 0xED || tile.id_ == 0xFC || tile.id_ == 0xFD)) {
LOG_DEBUG("[DrawTile]", "Floor tile 0x%02X at sheet pos (%d,%d), palette=%d, mirror=(%d,%d)",
tile.id_, tile_x, tile_y, tile.palette_, tile.horizontal_mirror_, tile.vertical_mirror_);
LOG_DEBUG("[DrawTile]", "First row (8 pixels): ");
for (int i = 0; i < 8; i++) {
int src_index = tile_y * 128 + (tile_x + i);
LOG_DEBUG("[DrawTile]", "%d ", tiledata[src_index]);
}
LOG_DEBUG("[DrawTile]", "Second row (8 pixels): ");
for (int i = 0; i < 8; i++) {
int src_index = (tile_y + 1) * 128 + (tile_x + i);
LOG_DEBUG("[DrawTile]", "%d ", tiledata[src_index]);
}
debug_count++;
}
// Dungeon graphics are 3BPP: 8 colors per palette (0-7, 8-15, 16-23, etc.)
// NOT 4BPP which would be 16 colors per palette!
// Clamp palette to 0-10 (90 colors / 8 = 11.25, so max palette is 10)
uint8_t clamped_palette = tile.palette_ & 0x0F;
if (clamped_palette > 10) {
clamped_palette = clamped_palette % 11;
}
// For 3BPP: palette offset = palette * 8 (not * 16!)
uint8_t palette_offset = (uint8_t)(clamped_palette * 8);
// Copy 8x8 pixels from tiledata to canvas
for (int py = 0; py < 8; py++) {
for (int px = 0; px < 8; px++) {
// Apply mirroring
int src_x = tile.horizontal_mirror_ ? (7 - px) : px;
int src_y = tile.vertical_mirror_ ? (7 - py) : py;
// Read pixel from tiledata (128-pixel-wide bitmap)
int src_index = (tile_y + src_y) * 128 + (tile_x + src_x);
uint8_t pixel_index = tiledata[src_index];
// Apply palette offset and write to canvas
// For 3BPP: final color = base_pixel (0-7) + palette_offset (0, 8, 16, 24, ...)
if (pixel_index == 0) {
continue;
}
uint8_t final_color = pixel_index + palette_offset;
int dest_index = indexoffset + (py * width_) + px;
canvas[dest_index] = final_color;
}
}
}
void BackgroundBuffer::DrawBackground(std::span<uint8_t> gfx16_data) {
int tiles_w = width_ / 8;
int tiles_h = height_ / 8;
if ((int)buffer_.size() < tiles_w * tiles_h) {
buffer_.resize(tiles_w * tiles_h);
}
// NEVER recreate bitmap here - it should be created by DrawFloor or initialized earlier
// If bitmap doesn't exist, create it ONCE with zeros
if (!bitmap_.is_active() || bitmap_.width() == 0) {
bitmap_.Create(width_, height_, 8, std::vector<uint8_t>(width_ * height_, 0));
}
// For each tile on the tile buffer
int drawn_count = 0;
int skipped_count = 0;
for (int yy = 0; yy < tiles_h; yy++) {
for (int xx = 0; xx < tiles_w; xx++) {
uint16_t word = buffer_[xx + yy * tiles_w];
// Skip empty tiles (0xFFFF) - these show the floor
if (word == 0xFFFF) {
skipped_count++;
continue;
}
// Skip zero tiles - also show the floor
if (word == 0) {
skipped_count++;
continue;
}
auto tile = gfx::WordToTileInfo(word);
// Skip floor tiles (0xEC-0xFD) - don't overwrite DrawFloor's work
// These are the animated floor tiles, already drawn by DrawFloor
if (tile.id_ >= 0xEC && tile.id_ <= 0xFD) {
skipped_count++;
continue;
}
// Calculate pixel offset for tile position (xx, yy) in the 512x512 bitmap
// Each tile is 8x8, so pixel Y = yy * 8, pixel X = xx * 8
// Linear offset = (pixel_y * width) + pixel_x = (yy * 8 * 512) + (xx * 8)
int tile_offset = (yy * 8 * width_) + (xx * 8);
DrawTile(tile, bitmap_.mutable_data().data(), gfx16_data.data(), tile_offset);
drawn_count++;
}
}
// CRITICAL: Sync bitmap data back to SDL surface!
// DrawTile() writes to bitmap_.mutable_data(), but the SDL surface needs updating
if (bitmap_.surface() && bitmap_.mutable_data().size() > 0) {
SDL_LockSurface(bitmap_.surface());
memcpy(bitmap_.surface()->pixels, bitmap_.mutable_data().data(), bitmap_.mutable_data().size());
SDL_UnlockSurface(bitmap_.surface());
}
}
void BackgroundBuffer::DrawFloor(const std::vector<uint8_t>& rom_data,
int tile_address, int tile_address_floor,
uint8_t floor_graphics) {
// Create bitmap ONCE at the start if it doesn't exist
if (!bitmap_.is_active() || bitmap_.width() == 0) {
LOG_DEBUG("[DrawFloor]", "Creating bitmap: %dx%d, active=%d, width=%d",
width_, height_, bitmap_.is_active(), bitmap_.width());
bitmap_.Create(width_, height_, 8, std::vector<uint8_t>(width_ * height_, 0));
LOG_DEBUG("[DrawFloor]", "After Create: active=%d, width=%d, height=%d",
bitmap_.is_active(), bitmap_.width(), bitmap_.height());
} else {
LOG_DEBUG("[DrawFloor]", "Bitmap already exists: active=%d, width=%d, height=%d",
bitmap_.is_active(), bitmap_.width(), bitmap_.height());
}
auto f = (uint8_t)(floor_graphics << 4);
// Create floor tiles from ROM data
gfx::TileInfo floorTile1(rom_data[tile_address + f],
rom_data[tile_address + f + 1]);
gfx::TileInfo floorTile2(rom_data[tile_address + f + 2],
rom_data[tile_address + f + 3]);
gfx::TileInfo floorTile3(rom_data[tile_address + f + 4],
rom_data[tile_address + f + 5]);
gfx::TileInfo floorTile4(rom_data[tile_address + f + 6],
rom_data[tile_address + f + 7]);
gfx::TileInfo floorTile5(rom_data[tile_address_floor + f],
rom_data[tile_address_floor + f + 1]);
gfx::TileInfo floorTile6(rom_data[tile_address_floor + f + 2],
rom_data[tile_address_floor + f + 3]);
gfx::TileInfo floorTile7(rom_data[tile_address_floor + f + 4],
rom_data[tile_address_floor + f + 5]);
gfx::TileInfo floorTile8(rom_data[tile_address_floor + f + 6],
rom_data[tile_address_floor + f + 7]);
// Floor tiles specify which 8-color sub-palette from the 90-color dungeon palette
// e.g., palette 6 = colors 48-55 (6 * 8 = 48)
// Draw the floor tiles in a pattern
// Convert TileInfo to 16-bit words with palette information
uint16_t word1 = gfx::TileInfoToWord(floorTile1);
uint16_t word2 = gfx::TileInfoToWord(floorTile2);
uint16_t word3 = gfx::TileInfoToWord(floorTile3);
uint16_t word4 = gfx::TileInfoToWord(floorTile4);
uint16_t word5 = gfx::TileInfoToWord(floorTile5);
uint16_t word6 = gfx::TileInfoToWord(floorTile6);
uint16_t word7 = gfx::TileInfoToWord(floorTile7);
uint16_t word8 = gfx::TileInfoToWord(floorTile8);
for (int xx = 0; xx < 16; xx++) {
for (int yy = 0; yy < 32; yy++) {
SetTileAt((xx * 4), (yy * 2), word1);
SetTileAt((xx * 4) + 1, (yy * 2), word2);
SetTileAt((xx * 4) + 2, (yy * 2), word3);
SetTileAt((xx * 4) + 3, (yy * 2), word4);
SetTileAt((xx * 4), (yy * 2) + 1, word5);
SetTileAt((xx * 4) + 1, (yy * 2) + 1, word6);
SetTileAt((xx * 4) + 2, (yy * 2) + 1, word7);
SetTileAt((xx * 4) + 3, (yy * 2) + 1, word8);
}
}
}
} // namespace yaze::gfx

View File

@@ -0,0 +1,45 @@
#ifndef YAZE_APP_GFX_BACKGROUND_BUFFER_H
#define YAZE_APP_GFX_BACKGROUND_BUFFER_H
#include <cstdint>
#include <vector>
#include "app/gfx/core/bitmap.h"
#include "app/gfx/types/snes_tile.h"
namespace yaze {
namespace gfx {
class BackgroundBuffer {
public:
BackgroundBuffer(int width = 512, int height = 512);
// Buffer manipulation methods
void SetTileAt(int x, int y, uint16_t value);
uint16_t GetTileAt(int x, int y) const;
void ClearBuffer();
// Drawing methods
void DrawTile(const TileInfo& tile_info, uint8_t* canvas,
const uint8_t* tiledata, int indexoffset);
void DrawBackground(std::span<uint8_t> gfx16_data);
// Floor drawing methods
void DrawFloor(const std::vector<uint8_t>& rom_data, int tile_address,
int tile_address_floor, uint8_t floor_graphics);
// Accessors
auto buffer() { return buffer_; }
auto& bitmap() { return bitmap_; }
private:
std::vector<uint16_t> buffer_;
gfx::Bitmap bitmap_;
int width_;
int height_;
};
} // namespace gfx
} // namespace yaze
#endif // YAZE_APP_GFX_BACKGROUND_BUFFER_H

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

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/core/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

@@ -0,0 +1,380 @@
#include "app/gfx/render/tilemap.h"
#include <vector>
#include "app/gfx/resource/arena.h"
#include "app/gfx/render/atlas_renderer.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/debug/performance/performance_profiler.h"
#include "app/gfx/types/snes_tile.h"
namespace yaze {
namespace gfx {
Tilemap CreateTilemap(IRenderer* renderer, std::vector<uint8_t> &data, int width, int height,
int tile_size, int num_tiles, SnesPalette &palette) {
Tilemap tilemap;
tilemap.tile_size.x = tile_size;
tilemap.tile_size.y = tile_size;
tilemap.map_size.x = num_tiles;
tilemap.map_size.y = num_tiles;
tilemap.atlas = Bitmap(width, height, 8, data);
tilemap.atlas.SetPalette(palette);
// 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(IRenderer* renderer, Tilemap &tilemap, const std::vector<uint8_t> &data) {
tilemap.atlas.set_data(data);
// 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(IRenderer* renderer, Tilemap &tilemap, int tile_id) {
// Validate tilemap state before proceeding
if (!tilemap.atlas.is_active() || tilemap.atlas.vector().empty()) {
return;
}
if (tile_id < 0) {
return;
}
// Get tile data without using problematic tile cache
auto tile_data = GetTilemapData(tilemap, tile_id);
if (tile_data.empty()) {
return;
}
// Note: Tile cache disabled to prevent std::move() related crashes
}
void RenderTile16(IRenderer* renderer, Tilemap &tilemap, int tile_id) {
// Validate tilemap state before proceeding
if (!tilemap.atlas.is_active() || tilemap.atlas.vector().empty()) {
return;
}
if (tile_id < 0) {
return;
}
int tiles_per_row = tilemap.atlas.width() / tilemap.tile_size.x;
if (tiles_per_row <= 0) {
return;
}
int tile_x = (tile_id % tiles_per_row) * tilemap.tile_size.x;
int tile_y = (tile_id / tiles_per_row) * tilemap.tile_size.y;
// Validate tile position
if (tile_x < 0 || tile_x >= tilemap.atlas.width() ||
tile_y < 0 || tile_y >= tilemap.atlas.height()) {
return;
}
// Note: Tile cache disabled to prevent std::move() related crashes
}
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) {
// Update cached tile data
int tiles_per_row = tilemap.atlas.width() / tilemap.tile_size.x;
int tile_x = (tile_id % tiles_per_row) * tilemap.tile_size.x;
int tile_y = (tile_id / tiles_per_row) * tilemap.tile_size.y;
std::vector<uint8_t> tile_data(tilemap.tile_size.x * tilemap.tile_size.y, 0x00);
int tile_data_offset = 0;
tilemap.atlas.Get16x16Tile(tile_x, tile_y, tile_data, tile_data_offset);
cached_tile->set_data(tile_data);
// 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(renderer, tilemap, tile_id);
}
}
std::vector<uint8_t> FetchTileDataFromGraphicsBuffer(
const std::vector<uint8_t> &data, int tile_id, int sheet_offset) {
const int tile_width = 8;
const int tile_height = 8;
const int buffer_width = 128;
const int sheet_height = 32;
const int tiles_per_row = buffer_width / tile_width;
const int rows_per_sheet = sheet_height / tile_height;
const int tiles_per_sheet = tiles_per_row * rows_per_sheet;
int sheet = (tile_id / tiles_per_sheet) % 4 + sheet_offset;
int position_in_sheet = tile_id % tiles_per_sheet;
int row_in_sheet = position_in_sheet / tiles_per_row;
int column_in_sheet = position_in_sheet % tiles_per_row;
assert(sheet >= sheet_offset && sheet <= sheet_offset + 3);
std::vector<uint8_t> tile_data(tile_width * tile_height);
for (int y = 0; y < tile_height; ++y) {
for (int x = 0; x < tile_width; ++x) {
int src_x = column_in_sheet * tile_width + x;
int src_y = (sheet * sheet_height) + (row_in_sheet * tile_height) + y;
int src_index = (src_y * buffer_width) + src_x;
int dest_index = y * tile_width + x;
tile_data[dest_index] = data[src_index];
}
}
return tile_data;
}
namespace {
void MirrorTileDataVertically(std::vector<uint8_t> &tile_data) {
for (int y = 0; y < 4; ++y) {
for (int x = 0; x < 8; ++x) {
std::swap(tile_data[y * 8 + x], tile_data[(7 - y) * 8 + x]);
}
}
}
void MirrorTileDataHorizontally(std::vector<uint8_t> &tile_data) {
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 4; ++x) {
std::swap(tile_data[y * 8 + x], tile_data[y * 8 + (7 - x)]);
}
}
}
void ComposeAndPlaceTilePart(Tilemap &tilemap, const std::vector<uint8_t> &data,
const TileInfo &tile_info, int base_x, int base_y,
int sheet_offset) {
std::vector<uint8_t> tile_data =
FetchTileDataFromGraphicsBuffer(data, tile_info.id_, sheet_offset);
if (tile_info.vertical_mirror_) {
MirrorTileDataVertically(tile_data);
}
if (tile_info.horizontal_mirror_) {
MirrorTileDataHorizontally(tile_data);
}
for (int y = 0; y < 8; ++y) {
for (int x = 0; x < 8; ++x) {
int src_index = y * 8 + x;
int dest_x = base_x + x;
int dest_y = base_y + y;
int dest_index = (dest_y * tilemap.atlas.width()) + dest_x;
tilemap.atlas.WriteToPixel(dest_index, tile_data[src_index]);
}
};
}
} // namespace
void ModifyTile16(Tilemap &tilemap, const std::vector<uint8_t> &data,
const TileInfo &top_left, const TileInfo &top_right,
const TileInfo &bottom_left, const TileInfo &bottom_right,
int sheet_offset, int tile_id) {
// Calculate the base position for this Tile16 in the full-size bitmap
int tiles_per_row = tilemap.atlas.width() / tilemap.tile_size.x;
int tile16_row = tile_id / tiles_per_row;
int tile16_column = tile_id % tiles_per_row;
int base_x = tile16_column * tilemap.tile_size.x;
int base_y = tile16_row * tilemap.tile_size.y;
// Compose and place each part of the Tile16
ComposeAndPlaceTilePart(tilemap, data, top_left, base_x, base_y,
sheet_offset);
ComposeAndPlaceTilePart(tilemap, data, top_right, base_x + 8, base_y,
sheet_offset);
ComposeAndPlaceTilePart(tilemap, data, bottom_left, base_x, base_y + 8,
sheet_offset);
ComposeAndPlaceTilePart(tilemap, data, bottom_right, base_x + 8, base_y + 8,
sheet_offset);
tilemap.tile_info[tile_id] = {top_left, top_right, bottom_left, bottom_right};
}
void ComposeTile16(Tilemap &tilemap, const std::vector<uint8_t> &data,
const TileInfo &top_left, const TileInfo &top_right,
const TileInfo &bottom_left, const TileInfo &bottom_right,
int sheet_offset) {
int num_tiles = tilemap.tile_info.size();
int tiles_per_row = tilemap.atlas.width() / tilemap.tile_size.x;
int tile16_row = num_tiles / tiles_per_row;
int tile16_column = num_tiles % tiles_per_row;
int base_x = tile16_column * tilemap.tile_size.x;
int base_y = tile16_row * tilemap.tile_size.y;
ComposeAndPlaceTilePart(tilemap, data, top_left, base_x, base_y,
sheet_offset);
ComposeAndPlaceTilePart(tilemap, data, top_right, base_x + 8, base_y,
sheet_offset);
ComposeAndPlaceTilePart(tilemap, data, bottom_left, base_x, base_y + 8,
sheet_offset);
ComposeAndPlaceTilePart(tilemap, data, bottom_right, base_x + 8, base_y + 8,
sheet_offset);
tilemap.tile_info.push_back({top_left, top_right, bottom_left, bottom_right});
}
std::vector<uint8_t> GetTilemapData(Tilemap &tilemap, int tile_id) {
// Comprehensive validation to prevent crashes
if (tile_id < 0) {
SDL_Log("GetTilemapData: Invalid tile_id %d (negative)", tile_id);
return std::vector<uint8_t>(256, 0); // Return empty 16x16 tile data
}
if (!tilemap.atlas.is_active()) {
SDL_Log("GetTilemapData: Atlas is not active for tile_id %d", tile_id);
return std::vector<uint8_t>(256, 0); // Return empty 16x16 tile data
}
if (tilemap.atlas.vector().empty()) {
SDL_Log("GetTilemapData: Atlas vector is empty for tile_id %d", tile_id);
return std::vector<uint8_t>(256, 0); // Return empty 16x16 tile data
}
if (tilemap.tile_size.x <= 0 || tilemap.tile_size.y <= 0) {
SDL_Log("GetTilemapData: Invalid tile size (%d, %d) for tile_id %d",
tilemap.tile_size.x, tilemap.tile_size.y, tile_id);
return std::vector<uint8_t>(256, 0); // Return empty 16x16 tile data
}
int tile_size = tilemap.tile_size.x;
int width = tilemap.atlas.width();
int height = tilemap.atlas.height();
// Validate atlas dimensions
if (width <= 0 || height <= 0) {
SDL_Log("GetTilemapData: Invalid atlas dimensions (%d, %d) for tile_id %d",
width, height, tile_id);
return std::vector<uint8_t>(tile_size * tile_size, 0);
}
// Calculate maximum possible tile_id based on atlas size
int tiles_per_row = width / tile_size;
int tiles_per_column = height / tile_size;
int max_tile_id = tiles_per_row * tiles_per_column - 1;
if (tile_id > max_tile_id) {
SDL_Log("GetTilemapData: tile_id %d exceeds maximum %d (atlas: %dx%d, tile_size: %d)",
tile_id, max_tile_id, width, height, tile_size);
return std::vector<uint8_t>(tile_size * tile_size, 0);
}
std::vector<uint8_t> data(tile_size * tile_size);
for (int ty = 0; ty < tile_size; ty++) {
for (int tx = 0; tx < tile_size; tx++) {
// Calculate atlas position more safely
int tile_row = tile_id / tiles_per_row;
int tile_col = tile_id % tiles_per_row;
int atlas_x = tile_col * tile_size + tx;
int atlas_y = tile_row * tile_size + ty;
int atlas_index = atlas_y * width + atlas_x;
// Comprehensive bounds checking
if (atlas_x >= 0 && atlas_x < width &&
atlas_y >= 0 && atlas_y < height &&
atlas_index >= 0 && atlas_index < static_cast<int>(tilemap.atlas.vector().size())) {
uint8_t value = tilemap.atlas.vector()[atlas_index];
data[ty * tile_size + tx] = value;
} else {
SDL_Log("GetTilemapData: Atlas position (%d, %d) or index %d out of bounds (atlas: %dx%d, size: %zu)",
atlas_x, atlas_y, atlas_index, width, height, tilemap.atlas.vector().size());
data[ty * tile_size + tx] = 0; // Default to 0 if out of bounds
}
}
}
return data;
}
void RenderTilesBatch(IRenderer* renderer, Tilemap& tilemap, const std::vector<int>& tile_ids,
const std::vector<std::pair<float, float>>& positions,
const std::vector<std::pair<float, float>>& scales) {
if (tile_ids.empty() || positions.empty() || tile_ids.size() != positions.size()) {
return;
}
ScopedTimer timer("tilemap_batch_render");
// Initialize atlas renderer if not already done
auto& atlas_renderer = AtlasRenderer::Get();
if (!renderer) {
// For now, we'll use the existing rendering approach
// In a full implementation, we'd get the renderer from the core system
return;
}
// Prepare render commands
std::vector<RenderCommand> render_commands;
render_commands.reserve(tile_ids.size());
for (size_t i = 0; i < tile_ids.size(); ++i) {
int tile_id = tile_ids[i];
float x = positions[i].first;
float y = positions[i].second;
// Get scale factors (default to 1.0 if not provided)
float scale_x = 1.0F;
float scale_y = 1.0F;
if (i < scales.size()) {
scale_x = scales[i].first;
scale_y = scales[i].second;
}
// Try to get tile from cache first
Bitmap* cached_tile = tilemap.tile_cache.GetTile(tile_id);
if (!cached_tile) {
// Create and cache the tile if not found
gfx::Bitmap new_tile = gfx::Bitmap(
tilemap.tile_size.x, tilemap.tile_size.y, 8,
gfx::GetTilemapData(tilemap, tile_id), tilemap.atlas.palette());
tilemap.tile_cache.CacheTile(tile_id, std::move(new_tile));
cached_tile = tilemap.tile_cache.GetTile(tile_id);
if (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) {
render_commands.emplace_back(atlas_id, x, y, scale_x, scale_y);
}
}
}
// Render all commands in batch
if (!render_commands.empty()) {
atlas_renderer.RenderBatch(render_commands);
}
}
} // namespace gfx
} // namespace yaze

View File

@@ -0,0 +1,157 @@
#ifndef YAZE_GFX_TILEMAP_H
#define YAZE_GFX_TILEMAP_H
#include "absl/container/flat_hash_map.h"
#include "app/gfx/backend/irenderer.h"
#include "app/gfx/core/bitmap.h"
#include "app/gfx/types/snes_tile.h"
#include <list>
#include <unordered_map>
namespace yaze {
namespace gfx {
/**
* @brief Simple 2D coordinate pair for tilemap dimensions
*/
struct Pair {
int x; ///< X coordinate or width
int y; ///< Y coordinate or height
};
/**
* @brief Smart tile cache with LRU eviction for efficient memory management
*
* Performance Optimizations:
* - LRU eviction policy to keep frequently used tiles in memory
* - Configurable cache size to balance memory usage and performance
* - O(1) tile access and insertion
* - Automatic cache management with minimal overhead
*/
struct TileCache {
static constexpr size_t MAX_CACHE_SIZE = 1024;
std::unordered_map<int, Bitmap> cache_;
std::list<int> access_order_;
/**
* @brief Get a cached tile by ID
* @param tile_id Tile identifier
* @return Pointer to cached tile bitmap or nullptr if not cached
*/
Bitmap* GetTile(int tile_id) {
auto it = cache_.find(tile_id);
if (it != cache_.end()) {
// Move to front of access order (most recently used)
access_order_.remove(tile_id);
access_order_.push_front(tile_id);
return &it->second;
}
return nullptr;
}
/**
* @brief Cache a tile bitmap
* @param tile_id Tile identifier
* @param bitmap Tile bitmap to cache
*/
void CacheTile(int tile_id, Bitmap&& bitmap) {
if (cache_.size() >= MAX_CACHE_SIZE) {
// Remove least recently used tile
int lru_tile = access_order_.back();
access_order_.pop_back();
cache_.erase(lru_tile);
}
cache_[tile_id] = std::move(bitmap);
access_order_.push_front(tile_id);
}
/**
* @brief Clear the cache
*/
void Clear() {
cache_.clear();
access_order_.clear();
}
/**
* @brief Get cache statistics
* @return Number of cached tiles
*/
size_t Size() const { return cache_.size(); }
};
/**
* @brief Tilemap structure for SNES tile-based graphics management
*
* The Tilemap class provides comprehensive tile management for ROM hacking:
*
* Key Features:
* - Atlas bitmap containing all tiles in a single texture
* - Smart tile cache with LRU eviction for optimal memory usage
* - Tile metadata storage (mirroring, palette, etc.)
* - Support for both 8x8 and 16x16 tile sizes
* - Efficient tile lookup and rendering
*
* Performance Optimizations:
* - Hash map storage for O(1) tile access
* - LRU tile caching to minimize memory usage
* - Atlas-based rendering to minimize draw calls
* - Tile metadata caching for fast property access
*
* ROM Hacking Specific:
* - SNES tile format support (4BPP, 8BPP)
* - Tile mirroring and flipping support
* - Palette index management per tile
* - Integration with SNES graphics buffer format
*/
struct Tilemap {
Bitmap atlas; ///< Master bitmap containing all tiles
TileCache tile_cache; ///< Smart tile cache with LRU eviction
std::vector<std::array<gfx::TileInfo, 4>> tile_info; ///< Tile metadata (4 tiles per 16x16)
Pair tile_size; ///< Size of individual tiles (8x8 or 16x16)
Pair map_size; ///< Size of tilemap in tiles
};
std::vector<uint8_t> FetchTileDataFromGraphicsBuffer(
const std::vector<uint8_t> &data, int tile_id, int sheet_offset);
Tilemap CreateTilemap(IRenderer* renderer, std::vector<uint8_t> &data, int width, int height,
int tile_size, int num_tiles, SnesPalette &palette);
void UpdateTilemap(IRenderer* renderer, Tilemap &tilemap, const std::vector<uint8_t> &data);
void RenderTile(IRenderer* renderer, 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<uint8_t> &data,
const TileInfo &top_left, const TileInfo &top_right,
const TileInfo &bottom_left, const TileInfo &bottom_right,
int sheet_offset, int tile_id);
void ComposeTile16(Tilemap &tilemap, const std::vector<uint8_t> &data,
const TileInfo &top_left, const TileInfo &top_right,
const TileInfo &bottom_left, const TileInfo &bottom_right,
int sheet_offset);
std::vector<uint8_t> GetTilemapData(Tilemap &tilemap, int tile_id);
/**
* @brief Render multiple tiles using atlas rendering for improved performance
* @param tilemap Tilemap containing tiles to render
* @param tile_ids Vector of tile IDs to render
* @param positions Vector of screen positions for each tile
* @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(IRenderer* renderer, Tilemap& tilemap, const std::vector<int>& tile_ids,
const std::vector<std::pair<float, float>>& positions,
const std::vector<std::pair<float, float>>& scales = {});
} // namespace gfx
} // namespace yaze
#endif // YAZE_GFX_TILEMAP_H