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:
454
src/app/gfx/render/atlas_renderer.cc
Normal file
454
src/app/gfx/render/atlas_renderer.cc
Normal 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
|
||||
206
src/app/gfx/render/atlas_renderer.h
Normal file
206
src/app/gfx/render/atlas_renderer.h
Normal 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
|
||||
218
src/app/gfx/render/background_buffer.cc
Normal file
218
src/app/gfx/render/background_buffer.cc
Normal 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
|
||||
45
src/app/gfx/render/background_buffer.h
Normal file
45
src/app/gfx/render/background_buffer.h
Normal 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
|
||||
152
src/app/gfx/render/texture_atlas.cc
Normal file
152
src/app/gfx/render/texture_atlas.cc
Normal 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 ®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<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/render/texture_atlas.h
Normal file
150
src/app/gfx/render/texture_atlas.h
Normal 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
|
||||
|
||||
380
src/app/gfx/render/tilemap.cc
Normal file
380
src/app/gfx/render/tilemap.cc
Normal 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
|
||||
157
src/app/gfx/render/tilemap.h
Normal file
157
src/app/gfx/render/tilemap.h
Normal 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
|
||||
Reference in New Issue
Block a user