epic: refactor SDL2_Renderer usage to IRenderer and queued texture rendering

- 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.
This commit is contained in:
scawful
2025-10-07 17:15:11 -04:00
parent 9e6f538520
commit 6c331f1fd0
101 changed files with 1401 additions and 2677 deletions

View File

@@ -3,11 +3,14 @@
#include <SDL.h>
#include <algorithm>
#include "app/gfx/backend/irenderer.h"
#include "util/sdl_deleter.h"
namespace yaze {
namespace gfx {
void Arena::Initialize(IRenderer* renderer) { renderer_ = renderer; }
Arena& Arena::Get() {
static Arena instance;
return instance;
@@ -23,74 +26,95 @@ Arena::~Arena() {
Shutdown();
}
/**
* @brief Allocate a new SDL texture with automatic cleanup and resource pooling
* @param renderer SDL renderer for texture creation
* @param width Texture width in pixels
* @param height Texture height in pixels
* @return Pointer to allocated texture (managed by Arena)
*
* Performance Notes:
* - Uses RGBA8888 format for maximum compatibility
* - STREAMING access for dynamic updates (common in ROM editing)
* - Resource pooling for 30% memory reduction
* - Automatic cleanup via unique_ptr with custom deleter
* - Hash map storage for O(1) lookup and management
*/
SDL_Texture* Arena::AllocateTexture(SDL_Renderer* renderer, int width,
int height) {
if (!renderer) {
SDL_Log("Invalid renderer passed to AllocateTexture");
return nullptr;
}
if (width <= 0 || height <= 0) {
SDL_Log("Invalid texture dimensions: width=%d, height=%d", width, height);
return nullptr;
}
// Try to reuse existing texture of same size from pool
for (auto it = texture_pool_.available_textures_.begin();
it != texture_pool_.available_textures_.end(); ++it) {
auto& size = texture_pool_.texture_sizes_[*it];
if (size.first == width && size.second == height) {
SDL_Texture* texture = *it;
texture_pool_.available_textures_.erase(it);
// Store in hash map with automatic cleanup
textures_[texture] =
std::unique_ptr<SDL_Texture, util::SDL_Texture_Deleter>(texture);
return texture;
}
}
// Create new texture if none available in pool
return CreateNewTexture(renderer, width, height);
void Arena::QueueTextureCommand(TextureCommandType type, Bitmap* bitmap) {
texture_command_queue_.push_back({type, bitmap});
}
void Arena::FreeTexture(SDL_Texture* texture) {
if (!texture) return;
void Arena::ProcessTextureQueue(IRenderer* renderer) {
if (!renderer_) return;
auto it = textures_.find(texture);
if (it != textures_.end()) {
// Return to pool instead of destroying if pool has space
if (texture_pool_.available_textures_.size() < texture_pool_.MAX_POOL_SIZE) {
// Get texture dimensions before releasing
int width, height;
SDL_QueryTexture(texture, nullptr, nullptr, &width, &height);
texture_pool_.texture_sizes_[texture] = {width, height};
texture_pool_.available_textures_.push_back(texture);
// Release from unique_ptr without destroying
it->second.release();
for (const auto& command : texture_command_queue_) {
switch (command.type) {
case TextureCommandType::CREATE: {
// Create a new texture and update it with bitmap data
if (command.bitmap && command.bitmap->surface() &&
command.bitmap->surface()->format &&
command.bitmap->is_active() &&
command.bitmap->width() > 0 && command.bitmap->height() > 0) {
auto texture = renderer_->CreateTexture(command.bitmap->width(),
command.bitmap->height());
if (texture) {
command.bitmap->set_texture(texture);
renderer_->UpdateTexture(texture, *command.bitmap);
}
}
break;
}
case TextureCommandType::UPDATE: {
// Update existing texture with current bitmap data
if (command.bitmap && command.bitmap->texture() &&
command.bitmap->surface() && command.bitmap->surface()->format &&
command.bitmap->is_active()) {
renderer_->UpdateTexture(command.bitmap->texture(), *command.bitmap);
}
break;
}
case TextureCommandType::DESTROY: {
if (command.bitmap && command.bitmap->texture()) {
renderer_->DestroyTexture(command.bitmap->texture());
command.bitmap->set_texture(nullptr);
}
break;
}
}
textures_.erase(it);
}
texture_command_queue_.clear();
}
SDL_Surface* Arena::AllocateSurface(int width, int height, int depth, int format) {
// Try to get a surface from the pool first
for (auto it = surface_pool_.available_surfaces_.begin();
it != surface_pool_.available_surfaces_.end(); ++it) {
auto& info = surface_pool_.surface_info_[*it];
if (std::get<0>(info) == width && std::get<1>(info) == height &&
std::get<2>(info) == depth && std::get<3>(info) == format) {
SDL_Surface* surface = *it;
surface_pool_.available_surfaces_.erase(it);
return surface;
}
}
// Create new surface if none available in pool
Uint32 sdl_format = GetSnesPixelFormat(format);
SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, depth, sdl_format);
if (surface) {
auto surface_ptr = std::unique_ptr<SDL_Surface, util::SDL_Surface_Deleter>(surface);
surfaces_[surface] = std::move(surface_ptr);
surface_pool_.surface_info_[surface] = std::make_tuple(width, height, depth, format);
}
return surface;
}
void Arena::FreeSurface(SDL_Surface* surface) {
if (!surface) return;
// Return surface to pool if space available
if (surface_pool_.available_surfaces_.size() < surface_pool_.MAX_POOL_SIZE) {
surface_pool_.available_surfaces_.push_back(surface);
} else {
// Remove from tracking maps
surface_pool_.surface_info_.erase(surface);
surfaces_.erase(surface);
}
}
void Arena::Shutdown() {
// Process any remaining batch updates before shutdown
ProcessBatchTextureUpdates();
ProcessTextureQueue(renderer_);
// Clear pool references first to prevent reuse during shutdown
surface_pool_.available_surfaces_.clear();
@@ -104,406 +128,9 @@ void Arena::Shutdown() {
surfaces_.clear();
// Clear any remaining queue items
batch_update_queue_.clear();
texture_command_queue_.clear();
}
/**
* @brief Update texture data from surface (with format conversion)
* @param texture Target texture to update
* @param surface Source surface with pixel data
*
* Performance Notes:
* - Converts surface to RGBA8888 format for texture compatibility
* - Uses memcpy for efficient pixel data transfer
* - Handles format conversion automatically
* - Locks texture for direct pixel access
*
* ROM Hacking Specific:
* - Supports indexed color surfaces (common in SNES graphics)
* - Handles palette-based graphics conversion
* - Optimized for frequent updates during editing
*/
void Arena::UpdateTexture(SDL_Texture* texture, SDL_Surface* surface) {
if (!texture || !surface) {
SDL_Log("Invalid texture or surface passed to UpdateTexture");
return;
}
if (surface->pixels == nullptr) {
SDL_Log("Surface pixels are nullptr");
return;
}
// Additional safety checks to prevent crashes
if (surface->w <= 0 || surface->h <= 0) {
SDL_Log("Invalid surface dimensions: %dx%d", surface->w, surface->h);
return;
}
if (!surface->format) {
SDL_Log("Surface format is nullptr");
return;
}
// Convert surface to RGBA8888 format for texture compatibility
auto converted_surface =
std::unique_ptr<SDL_Surface, util::SDL_Surface_Deleter>(
SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA8888, 0),
util::SDL_Surface_Deleter());
if (!converted_surface) {
SDL_Log("SDL_ConvertSurfaceFormat failed: %s", SDL_GetError());
return;
}
// Additional validation for converted surface
if (!converted_surface->pixels) {
SDL_Log("Converted surface pixels are nullptr");
return;
}
if (converted_surface->w <= 0 || converted_surface->h <= 0) {
SDL_Log("Invalid converted surface dimensions: %dx%d", converted_surface->w, converted_surface->h);
return;
}
// Validate texture before locking
int texture_w, texture_h;
if (SDL_QueryTexture(texture, nullptr, nullptr, &texture_w, &texture_h) != 0) {
SDL_Log("SDL_QueryTexture failed: %s", SDL_GetError());
return;
}
if (texture_w != converted_surface->w || texture_h != converted_surface->h) {
SDL_Log("Texture/surface size mismatch: texture=%dx%d, surface=%dx%d",
texture_w, texture_h, converted_surface->w, converted_surface->h);
return;
}
// Lock texture for direct pixel access
void* pixels;
int pitch;
if (SDL_LockTexture(texture, nullptr, &pixels, &pitch) != 0) {
SDL_Log("SDL_LockTexture failed: %s", SDL_GetError());
return;
}
// Additional safety check for locked pixels
if (!pixels) {
SDL_Log("Locked texture pixels are nullptr");
SDL_UnlockTexture(texture);
return;
}
// Validate copy size to prevent buffer overrun
size_t copy_size = converted_surface->h * converted_surface->pitch;
size_t max_texture_size = texture_h * pitch;
if (copy_size > max_texture_size) {
SDL_Log("Copy size (%zu) exceeds texture capacity (%zu)", copy_size, max_texture_size);
SDL_UnlockTexture(texture);
return;
}
// Copy pixel data efficiently with bounds checking
memcpy(pixels, converted_surface->pixels, copy_size);
SDL_UnlockTexture(texture);
}
SDL_Surface* Arena::AllocateSurface(int width, int height, int depth,
int format) {
// Try to reuse existing surface of same size and format from pool
for (auto it = surface_pool_.available_surfaces_.begin();
it != surface_pool_.available_surfaces_.end(); ++it) {
auto& info = surface_pool_.surface_info_[*it];
if (std::get<0>(info) == width && std::get<1>(info) == height &&
std::get<2>(info) == depth && std::get<3>(info) == format) {
SDL_Surface* surface = *it;
surface_pool_.available_surfaces_.erase(it);
// Clear the surface pixels before reusing for safety
if (surface && surface->pixels) {
memset(surface->pixels, 0, surface->h * surface->pitch);
}
// Store in hash map with automatic cleanup
surfaces_[surface] =
std::unique_ptr<SDL_Surface, util::SDL_Surface_Deleter>(surface);
return surface;
}
}
// Create new surface if none available in pool
return CreateNewSurface(width, height, depth, format);
}
void Arena::FreeSurface(SDL_Surface* surface) {
if (!surface) return;
auto it = surfaces_.find(surface);
if (it != surfaces_.end()) {
// Return to pool instead of destroying if pool has space
if (surface_pool_.available_surfaces_.size() < surface_pool_.MAX_POOL_SIZE) {
// Get surface info before releasing
int width = surface->w;
int height = surface->h;
int depth = surface->format->BitsPerPixel;
int format = surface->format->format;
surface_pool_.surface_info_[surface] = {width, height, depth, format};
surface_pool_.available_surfaces_.push_back(surface);
// Release from unique_ptr without destroying
it->second.release();
}
surfaces_.erase(it);
}
}
/**
* @brief Create a new SDL texture (helper for resource pooling)
* @param renderer SDL renderer for texture creation
* @param width Texture width in pixels
* @param height Texture height in pixels
* @return Pointer to allocated texture (managed by Arena)
*/
SDL_Texture* Arena::CreateNewTexture(SDL_Renderer* renderer, int width, int height) {
SDL_Texture* texture =
SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_STREAMING, width, height);
if (!texture) {
SDL_Log("Failed to create texture: %s", SDL_GetError());
return nullptr;
}
// Store in hash map with automatic cleanup
textures_[texture] =
std::unique_ptr<SDL_Texture, util::SDL_Texture_Deleter>(texture);
return texture;
}
/**
* @brief Create a new SDL surface (helper for resource pooling)
* @param width Surface width in pixels
* @param height Surface height in pixels
* @param depth Color depth in bits per pixel
* @param format SDL pixel format
* @return Pointer to allocated surface (managed by Arena)
*/
SDL_Surface* Arena::CreateNewSurface(int width, int height, int depth, int format) {
SDL_Surface* surface =
SDL_CreateRGBSurfaceWithFormat(0, width, height, depth, format);
if (!surface) {
SDL_Log("Failed to create surface: %s", SDL_GetError());
return nullptr;
}
// Store in hash map with automatic cleanup
surfaces_[surface] =
std::unique_ptr<SDL_Surface, util::SDL_Surface_Deleter>(surface);
return surface;
}
/**
* @brief Update texture data from surface for a specific region
* @param texture Target texture to update
* @param surface Source surface with pixel data
* @param rect Region to update (nullptr for entire texture)
*
* Performance Notes:
* - Region-specific updates for efficiency
* - Converts surface to RGBA8888 format for texture compatibility
* - Uses memcpy for efficient pixel data transfer
* - Handles format conversion automatically
*/
void Arena::UpdateTextureRegion(SDL_Texture* texture, SDL_Surface* surface, SDL_Rect* rect) {
if (!texture || !surface) {
return;
}
if (surface->pixels == nullptr) {
return;
}
// Convert surface to RGBA8888 format for texture compatibility
auto converted_surface =
std::unique_ptr<SDL_Surface, util::SDL_Surface_Deleter>(
SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA8888, 0),
util::SDL_Surface_Deleter());
if (!converted_surface) {
return;
}
// Lock texture for direct pixel access
void* pixels;
int pitch;
if (SDL_LockTexture(texture, rect, &pixels, &pitch) != 0) {
return;
}
// Copy pixel data efficiently with bounds checking
if (rect) {
// Validate rect bounds against surface dimensions
int max_x = std::min(rect->x + rect->w, converted_surface->w);
int max_y = std::min(rect->y + rect->h, converted_surface->h);
int safe_x = std::max(0, rect->x);
int safe_y = std::max(0, rect->y);
int safe_w = max_x - safe_x;
int safe_h = max_y - safe_y;
if (safe_w > 0 && safe_h > 0) {
// Copy only the safe region
int src_offset = safe_y * converted_surface->pitch + safe_x * 4; // 4 bytes per RGBA pixel
int dst_offset = 0;
for (int y = 0; y < safe_h; y++) {
// Additional safety check for each row
if (src_offset + safe_w * 4 <= converted_surface->h * converted_surface->pitch) {
memcpy(static_cast<char*>(pixels) + dst_offset,
static_cast<char*>(converted_surface->pixels) + src_offset,
safe_w * 4);
}
src_offset += converted_surface->pitch;
dst_offset += pitch;
}
}
} else {
// Copy entire surface
memcpy(pixels, converted_surface->pixels,
converted_surface->h * converted_surface->pitch);
}
SDL_UnlockTexture(texture);
}
/**
* @brief Queue a texture update for batch processing
* @param texture Target texture to update
* @param surface Source surface with pixel data
* @param rect Region to update (nullptr for entire texture)
*
* Performance Notes:
* - Queues updates instead of processing immediately
* - Reduces SDL calls by batching multiple updates
* - Automatic queue size management to prevent memory bloat
*/
void Arena::QueueTextureUpdate(SDL_Texture* texture, SDL_Surface* surface, SDL_Rect* rect) {
if (!texture || !surface) {
SDL_Log("Invalid texture or surface passed to QueueTextureUpdate");
return;
}
// Prevent queue from growing too large
if (batch_update_queue_.size() >= MAX_BATCH_SIZE) {
ProcessBatchTextureUpdates();
}
batch_update_queue_.emplace_back(texture, surface, rect);
}
/**
* @brief Process all queued texture updates in a single batch
* @note This reduces SDL calls and improves performance significantly
*
* Performance Notes:
* - Processes all queued updates in one operation
* - Reduces SDL context switching overhead
* - Optimized for multiple small updates
* - Clears queue after processing
*/
void Arena::ProcessBatchTextureUpdates() {
if (batch_update_queue_.empty()) {
return;
}
// Process all queued updates with minimal logging
for (const auto& update : batch_update_queue_) {
// Validate pointers before processing
if (!update.texture || !update.surface) {
continue;
}
if (update.rect) {
UpdateTextureRegion(update.texture, update.surface, update.rect.get());
} else {
UpdateTexture(update.texture, update.surface);
}
}
// Clear the queue after processing
batch_update_queue_.clear();
}
/**
* @brief Clear all queued texture updates
* @note Useful for cleanup or when batch processing is not needed
*/
void Arena::ClearBatchQueue() {
batch_update_queue_.clear();
}
// ============================================================================
// Progressive/Deferred Texture Management
// ============================================================================
void Arena::QueueDeferredTexture(gfx::Bitmap* bitmap, int priority) {
if (!bitmap) return;
std::lock_guard<std::mutex> lock(deferred_mutex_);
deferred_textures_.emplace_back(bitmap, priority);
}
std::vector<gfx::Bitmap*> Arena::GetNextDeferredTextureBatch(
int high_priority_limit, int low_priority_limit) {
std::lock_guard<std::mutex> lock(deferred_mutex_);
std::vector<gfx::Bitmap*> batch;
if (deferred_textures_.empty()) {
return batch;
}
// Sort by priority (lower number = higher priority)
std::sort(deferred_textures_.begin(), deferred_textures_.end(),
[](const DeferredTexture& a, const DeferredTexture& b) {
return a.priority < b.priority;
});
// Phase 1: Collect high-priority items (priority 0-10)
auto it = deferred_textures_.begin();
while (it != deferred_textures_.end() && batch.size() < static_cast<size_t>(high_priority_limit)) {
if (it->bitmap && it->priority <= 10 && !it->bitmap->texture()) {
batch.push_back(it->bitmap);
it = deferred_textures_.erase(it);
} else {
++it;
}
}
// Phase 2: Collect low-priority items (priority 11+) if we have capacity
if (batch.size() < static_cast<size_t>(high_priority_limit)) {
it = deferred_textures_.begin();
int low_count = 0;
while (it != deferred_textures_.end() && low_count < low_priority_limit) {
if (it->bitmap && it->priority > 10 && !it->bitmap->texture()) {
batch.push_back(it->bitmap);
low_count++;
it = deferred_textures_.erase(it);
} else {
++it;
}
}
}
return batch;
}
void Arena::ClearDeferredTextures() {
std::lock_guard<std::mutex> lock(deferred_mutex_);
deferred_textures_.clear();
}
} // namespace gfx
} // namespace yaze