- Updated the testing guide to clarify the testing framework's organization and execution methods, improving user understanding. - Refactored CMakeLists to include new platform-specific files, ensuring proper integration of the rendering backend. - Modified main application files to utilize the new IRenderer interface, enhancing flexibility in rendering operations. - Implemented deferred texture management in various components, allowing for more efficient graphics handling and improved performance. - Introduced new methods for texture creation and updates, streamlining the rendering process across the application. - Enhanced logging and error handling in the rendering pipeline to facilitate better debugging and diagnostics.
455 lines
13 KiB
C++
455 lines
13 KiB
C++
#include "app/gfx/atlas_renderer.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include "app/gfx/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
|