feat(gpu): migrar a SDL3_GPU amb 2-pass rendering i post-processat

- Infraestructura GPU: GpuContext, GpuPipeline, GpuSpriteBatch, GpuTexture
- Engine::render() migrat a 2-pass: sprites → offscreen R8G8B8A8 → swapchain + vignette
- UI/text via software renderer (SDL3_ttf) + upload com a textura overlay GPU
- CMakeLists.txt actualitzat per incloure subsistema gpu/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 22:08:12 +01:00
parent 736db8cf41
commit 00a5875c92
11 changed files with 1341 additions and 240 deletions

View File

@@ -100,9 +100,9 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// SDL ya inicializado arriba para validación
{
// Crear ventana principal (fullscreen si se especifica)
// NOTA: SDL_WINDOW_HIGH_PIXEL_DENSITY removido por incompatibilidad con STRETCH mode (F4)
// El DPI se detectará manualmente con SDL_GetWindowSizeInPixels()
Uint32 window_flags = SDL_WINDOW_OPENGL;
// SDL_WINDOW_HIGH_PIXEL_DENSITY removido — DPI detectado con SDL_GetWindowSizeInPixels()
// SDL_WINDOW_OPENGL eliminado — SDL_GPU usa Metal/Vulkan/D3D12 directamente
Uint32 window_flags = 0;
if (fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN;
}
@@ -117,20 +117,24 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Crear renderizador
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
std::cout << "¡No se pudo crear el renderizador! Error de SDL: " << SDL_GetError() << std::endl;
// Inicializar SDL_GPU (sustituye SDL_Renderer como backend principal)
gpu_ctx_ = std::make_unique<GpuContext>();
if (!gpu_ctx_->init(window_)) {
std::cout << "¡No se pudo inicializar SDL_GPU!" << std::endl;
success = false;
} else {
// Establecer color inicial del renderizador
SDL_SetRenderDrawColor(renderer_, 0xFF, 0xFF, 0xFF, 0xFF);
gpu_ctx_->setVSync(vsync_enabled_);
// Establecer tamaño lógico para el renderizado (resolución interna)
SDL_SetRenderLogicalPresentation(renderer_, logical_width, logical_height, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Configurar V-Sync inicial
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
// Crear renderer de software para UI/texto (SDL3_ttf no es compatible con SDL_GPU)
// Renderiza a ui_surface_, que luego se sube como textura GPU overlay
ui_surface_ = SDL_CreateSurface(logical_width, logical_height, SDL_PIXELFORMAT_RGBA32);
if (ui_surface_) {
ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_);
}
if (!ui_renderer_) {
std::cout << "Advertencia: no se pudo crear el renderer de UI software" << std::endl;
// No es crítico — el juego funciona sin texto
}
}
}
}
@@ -143,7 +147,8 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
struct TextureInfo {
std::string name;
std::shared_ptr<Texture> texture;
std::shared_ptr<Texture> texture; // legacy (para physics sizing)
std::string path; // resource path para GPU upload
int width;
};
std::vector<TextureInfo> texture_files;
@@ -151,34 +156,32 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Buscar todas las texturas PNG en data/balls/
namespace fs = std::filesystem;
if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
// Cargar todas las texturas desde disco
for (const auto& entry : fs::directory_iterator(balls_dir)) {
if (entry.is_regular_file() && entry.path().extension() == ".png") {
std::string filename = entry.path().stem().string();
std::string fullpath = entry.path().string();
// Cargar textura y obtener dimensiones
auto texture = std::make_shared<Texture>(renderer_, fullpath);
// Cargar textura legacy (usa ui_renderer_ en lugar del renderer_ eliminado)
auto texture = std::make_shared<Texture>(ui_renderer_, fullpath);
int width = texture->getWidth();
texture_files.push_back({filename, texture, width});
texture_files.push_back({filename, texture, fullpath, width});
}
}
} else {
// Fallback: cargar texturas desde pack usando la lista del ResourceManager
// Fallback: cargar texturas desde pack
if (ResourceManager::isPackLoaded()) {
auto pack_resources = ResourceManager::getResourceList();
// Filtrar solo los recursos en balls/ con extensión .png
for (const auto& resource : pack_resources) {
if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") {
std::string tex_name = resource.substr(6); // Quitar "balls/"
std::string name = tex_name.substr(0, tex_name.find('.')); // Quitar extensión
std::string tex_name = resource.substr(6);
std::string name = tex_name.substr(0, tex_name.find('.'));
auto texture = std::make_shared<Texture>(renderer_, resource);
auto texture = std::make_shared<Texture>(ui_renderer_, resource);
int width = texture->getWidth();
texture_files.push_back({name, texture, width});
texture_files.push_back({name, texture, resource, width});
}
}
}
@@ -186,13 +189,20 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4)
std::sort(texture_files.begin(), texture_files.end(), [](const TextureInfo& a, const TextureInfo& b) {
return a.width > b.width; // Descendente por tamaño
return a.width > b.width;
});
// Guardar texturas ya cargadas en orden (0=big, 1=normal, 2=small, 3=tiny)
// Guardar texturas en orden + crear texturas GPU
for (const auto& info : texture_files) {
textures_.push_back(info.texture);
texture_names_.push_back(info.name);
// Cargar textura GPU para renderizado de sprites
auto gpu_tex = std::make_unique<GpuTexture>();
if (gpu_ctx_ && !gpu_tex->fromFile(gpu_ctx_->device(), info.path)) {
std::cerr << "Advertencia: no se pudo cargar textura GPU: " << info.name << std::endl;
}
gpu_textures_.push_back(std::move(gpu_tex));
}
// Verificar que se cargaron texturas
@@ -201,16 +211,54 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
success = false;
}
// Buscar índice de "normal" para usarlo como textura inicial (debería ser índice 1)
current_texture_index_ = 0; // Fallback
// Buscar índice de "normal" para usarlo como textura inicial
current_texture_index_ = 0;
for (size_t i = 0; i < texture_names_.size(); i++) {
if (texture_names_[i] == "normal") {
current_texture_index_ = i; // Iniciar en "normal" (índice 1)
current_texture_index_ = i;
break;
}
}
texture_ = textures_[current_texture_index_];
current_ball_size_ = texture_->getWidth(); // Obtener tamaño dinámicamente
current_ball_size_ = texture_->getWidth();
// Initialize GPU pipeline, sprite batch, and render textures
if (gpu_ctx_ && success) {
SDL_GPUTextureFormat offscreen_fmt = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
gpu_pipeline_ = std::make_unique<GpuPipeline>();
if (!gpu_pipeline_->init(gpu_ctx_->device(), gpu_ctx_->swapchainFormat(), offscreen_fmt)) {
std::cerr << "ERROR: No se pudo crear el pipeline GPU" << std::endl;
success = false;
}
sprite_batch_ = std::make_unique<GpuSpriteBatch>();
if (!sprite_batch_->init(gpu_ctx_->device())) {
std::cerr << "ERROR: No se pudo crear el sprite batch GPU" << std::endl;
success = false;
}
offscreen_tex_ = std::make_unique<GpuTexture>();
if (!offscreen_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_,
offscreen_fmt)) {
std::cerr << "ERROR: No se pudo crear render target offscreen" << std::endl;
success = false;
}
white_tex_ = std::make_unique<GpuTexture>();
if (!white_tex_->createWhite(gpu_ctx_->device())) {
std::cerr << "ERROR: No se pudo crear textura blanca" << std::endl;
success = false;
}
// Create UI overlay texture (render target usage so GPU can sample it)
ui_tex_ = std::make_unique<GpuTexture>();
if (!ui_tex_->createRenderTarget(gpu_ctx_->device(),
logical_width, logical_height,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM)) {
std::cerr << "Advertencia: no se pudo crear textura UI GPU" << std::endl;
}
}
srand(static_cast<unsigned>(time(nullptr)));
@@ -240,7 +288,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Inicializar UIManager (HUD, FPS, notificaciones)
// NOTA: Debe llamarse DESPUÉS de calcular physical_window_* y ThemeManager
ui_manager_ = std::make_unique<UIManager>();
ui_manager_->initialize(renderer_, theme_manager_.get(),
ui_manager_->initialize(ui_renderer_, theme_manager_.get(),
physical_window_width_, physical_window_height_,
current_screen_width_, current_screen_height_);
@@ -280,7 +328,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMod
// Inicializar AppLogo (logo periódico en pantalla)
app_logo_ = std::make_unique<AppLogo>();
if (!app_logo_->initialize(renderer_, current_screen_width_, current_screen_height_)) {
if (!app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_)) {
std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << std::endl;
// No es crítico, continuar sin logo
app_logo_.reset();
@@ -318,11 +366,28 @@ void Engine::run() {
}
void Engine::shutdown() {
// Limpiar recursos SDL
if (renderer_) {
SDL_DestroyRenderer(renderer_);
renderer_ = nullptr;
// Wait for GPU idle before releasing GPU resources
if (gpu_ctx_) SDL_WaitForGPUIdle(gpu_ctx_->device());
// Release GPU sprite textures
gpu_textures_.clear();
// Release GPU render targets and utility textures
if (gpu_ctx_) {
if (ui_tex_) { ui_tex_->destroy(gpu_ctx_->device()); ui_tex_.reset(); }
if (white_tex_) { white_tex_->destroy(gpu_ctx_->device()); white_tex_.reset(); }
if (offscreen_tex_) { offscreen_tex_->destroy(gpu_ctx_->device()); offscreen_tex_.reset(); }
if (sprite_batch_) { sprite_batch_->destroy(gpu_ctx_->device()); sprite_batch_.reset(); }
if (gpu_pipeline_) { gpu_pipeline_->destroy(gpu_ctx_->device()); gpu_pipeline_.reset(); }
}
// Destroy software UI renderer and surface
if (ui_renderer_) { SDL_DestroyRenderer(ui_renderer_); ui_renderer_ = nullptr; }
if (ui_surface_) { SDL_DestroySurface(ui_surface_); ui_surface_ = nullptr; }
// Destroy GPU context (releases device and window claim)
if (gpu_ctx_) { gpu_ctx_->destroy(); gpu_ctx_.reset(); }
if (window_) {
SDL_DestroyWindow(window_);
window_ = nullptr;
@@ -636,140 +701,148 @@ void Engine::toggleLogoMode() {
}
void Engine::render() {
// Limpiar framebuffer completamente (evita artefactos en barras negras al cambiar modos)
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); // Negro para barras de letterbox/integer
SDL_RenderClear(renderer_);
if (!gpu_ctx_ || !sprite_batch_ || !gpu_pipeline_) return;
// Renderizar fondo degradado (delegado a ThemeManager)
{
float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b;
theme_manager_->getBackgroundColors(top_r, top_g, top_b, bottom_r, bottom_g, bottom_b);
// === Render UI text to software surface ===
renderUIToSurface();
// Crear quad de pantalla completa con degradado
SDL_Vertex bg_vertices[4];
// === Acquire command buffer ===
SDL_GPUCommandBuffer* cmd = gpu_ctx_->acquireCommandBuffer();
if (!cmd) return;
// Vértice superior izquierdo
bg_vertices[0].position = {0, 0};
bg_vertices[0].tex_coord = {0.0f, 0.0f};
bg_vertices[0].color = {top_r, top_g, top_b, 1.0f};
// === Upload UI surface to GPU texture (inline copy pass) ===
uploadUISurface(cmd);
// Vértice superior derecho
bg_vertices[1].position = {static_cast<float>(current_screen_width_), 0};
bg_vertices[1].tex_coord = {1.0f, 0.0f};
bg_vertices[1].color = {top_r, top_g, top_b, 1.0f};
// === Build sprite batch ===
sprite_batch_->beginFrame();
// Vértice inferior derecho
bg_vertices[2].position = {static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_)};
bg_vertices[2].tex_coord = {1.0f, 1.0f};
bg_vertices[2].color = {bottom_r, bottom_g, bottom_b, 1.0f};
// Background gradient
float top_r = 0, top_g = 0, top_b = 0, bot_r = 0, bot_g = 0, bot_b = 0;
theme_manager_->getBackgroundColors(top_r, top_g, top_b, bot_r, bot_g, bot_b);
sprite_batch_->addBackground(
static_cast<float>(current_screen_width_), static_cast<float>(current_screen_height_),
top_r, top_g, top_b, bot_r, bot_g, bot_b);
// Vértice inferior izquierdo
bg_vertices[3].position = {0, static_cast<float>(current_screen_height_)};
bg_vertices[3].tex_coord = {0.0f, 1.0f};
bg_vertices[3].color = {bottom_r, bottom_g, bottom_b, 1.0f};
// Índices para 2 triángulos
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
// Renderizar sin textura (nullptr)
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
}
// Limpiar batches del frame anterior
batch_vertices_.clear();
batch_indices_.clear();
// Obtener referencia a las bolas desde SceneManager
// Sprites (balls)
const auto& balls = scene_manager_->getBalls();
if (current_mode_ == SimulationMode::SHAPE) {
// MODO FIGURA 3D: Ordenar por profundidad Z (Painter's Algorithm)
// Las pelotas con menor depth_brightness (más lejos/oscuras) se renderizan primero
// Bucket sort per profunditat Z (O(N) vs O(N log N))
// Bucket sort by depth Z (Painter's Algorithm)
for (size_t i = 0; i < balls.size(); i++) {
int b = static_cast<int>(balls[i]->getDepthBrightness() * (DEPTH_SORT_BUCKETS - 1));
depth_buckets_[std::clamp(b, 0, DEPTH_SORT_BUCKETS - 1)].push_back(i);
}
// Renderizar en orden de profundidad (bucket 0 = fons, bucket 255 = davant)
for (int b = 0; b < DEPTH_SORT_BUCKETS; b++) {
for (size_t idx : depth_buckets_[b]) {
SDL_FRect pos = balls[idx]->getPosition();
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
Color color = theme_manager_->getInterpolatedColor(idx);
float brightness = balls[idx]->getDepthBrightness();
float depth_scale = balls[idx]->getDepthScale();
// Mapear brightness de 0-1 a rango MIN-MAX
float brightness_factor = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
// Aplicar factor de brillo al color
int r_mod = static_cast<int>(color.r * brightness_factor);
int g_mod = static_cast<int>(color.g * brightness_factor);
int b_mod = static_cast<int>(color.b * brightness_factor);
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, r_mod, g_mod, b_mod, depth_scale);
float bf = (ROTOBALL_MIN_BRIGHTNESS + brightness * (ROTOBALL_MAX_BRIGHTNESS - ROTOBALL_MIN_BRIGHTNESS)) / 255.0f;
sprite_batch_->addSprite(pos.x, pos.y, pos.w, pos.h,
color.r / 255.0f * bf,
color.g / 255.0f * bf,
color.b / 255.0f * bf,
1.0f, depth_scale,
static_cast<float>(current_screen_width_),
static_cast<float>(current_screen_height_));
}
depth_buckets_[b].clear(); // netejar per al proper frame
depth_buckets_[b].clear();
}
} else {
// MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad)
const auto& balls = scene_manager_->getBalls();
size_t idx = 0;
for (auto& ball : balls) {
for (const auto& ball : balls) {
SDL_FRect pos = ball->getPosition();
Color color = theme_manager_->getInterpolatedColor(idx); // Usar color interpolado (LERP)
addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f);
Color color = theme_manager_->getInterpolatedColor(idx);
sprite_batch_->addSprite(pos.x, pos.y, pos.w, pos.h,
color.r / 255.0f, color.g / 255.0f, color.b / 255.0f,
1.0f, 1.0f,
static_cast<float>(current_screen_width_),
static_cast<float>(current_screen_height_));
idx++;
}
}
// Renderizar todas las bolas en una sola llamada
if (!batch_vertices_.empty()) {
SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast<int>(batch_vertices_.size()), batch_indices_.data(), static_cast<int>(batch_indices_.size()));
// UI overlay quad (drawn in Pass 2 over the postfx output)
sprite_batch_->addFullscreenOverlay();
// Upload batch to GPU buffers
if (!sprite_batch_->uploadBatch(gpu_ctx_->device(), cmd)) {
gpu_ctx_->submit(cmd);
return;
}
// SISTEMA DE TEXTO ANTIGUO DESHABILITADO
// Reemplazado completamente por el sistema de notificaciones (Notifier)
// El doble renderizado causaba que aparecieran textos duplicados detrás de las notificaciones
/*
if (show_text_) {
// Obtener datos del tema actual (delegado a ThemeManager)
int text_color_r, text_color_g, text_color_b;
theme_manager_->getCurrentThemeTextColor(text_color_r, text_color_g, text_color_b);
const char* theme_name_es = theme_manager_->getCurrentThemeNameES();
GpuTexture* sprite_tex = (!gpu_textures_.empty())
? gpu_textures_[current_texture_index_].get() : nullptr;
// Calcular espaciado dinámico
int line_height = text_renderer_.getTextHeight();
int margin = 8;
// === Pass 1: Render background + sprites to offscreen texture ===
if (offscreen_tex_ && offscreen_tex_->isValid() && sprite_tex && sprite_tex->isValid()) {
SDL_GPUColorTargetInfo ct = {};
ct.texture = offscreen_tex_->texture();
ct.load_op = SDL_GPU_LOADOP_CLEAR;
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f};
ct.store_op = SDL_GPU_STOREOP_STORE;
// Texto del número de pelotas con color del tema
text_renderer_.printPhysical(text_pos_, margin, text_.c_str(), text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
SDL_GPURenderPass* pass1 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
SDL_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->spritePipeline());
// Mostrar nombre del tema en castellano debajo del número de pelotas
// (solo si text_ NO es ya el nombre del tema, para evitar duplicación)
if (theme_name_es != nullptr && text_ != theme_name_es) {
int theme_text_width = text_renderer_.getTextWidth(theme_name_es);
int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente
int theme_y = margin + line_height; // Espaciado dinámico
SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
SDL_BindGPUVertexBuffers(pass1, 0, &vb, 1);
SDL_BindGPUIndexBuffer(pass1, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
// Texto del nombre del tema con el mismo color
text_renderer_.printPhysical(theme_x, theme_y, theme_name_es, text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
// Background (white texture tinted by vertex color)
if (white_tex_ && white_tex_->isValid() && sprite_batch_->bgIndexCount() > 0) {
SDL_GPUTextureSamplerBinding tsb = {white_tex_->texture(), white_tex_->sampler()};
SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
SDL_DrawGPUIndexedPrimitives(pass1, sprite_batch_->bgIndexCount(), 1, 0, 0, 0);
}
}
*/
// Renderizar UI (debug HUD, texto obsoleto, notificaciones) - delegado a UIManager
ui_manager_->render(renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(),
shape_manager_->getActiveShape(), shape_manager_->getConvergence(),
physical_window_width_, physical_window_height_, current_screen_width_);
// Sprites
if (sprite_batch_->spriteIndexCount() > 0) {
SDL_GPUTextureSamplerBinding tsb = {sprite_tex->texture(), sprite_tex->sampler()};
SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
SDL_DrawGPUIndexedPrimitives(pass1, sprite_batch_->spriteIndexCount(), 1,
sprite_batch_->spriteIndexOffset(), 0, 0);
}
// Renderizar AppLogo (logo periódico) - después de UI, antes de present
if (app_logo_) {
app_logo_->render();
SDL_EndGPURenderPass(pass1);
}
SDL_RenderPresent(renderer_);
// === Pass 2: PostFX (vignette) + UI overlay to swapchain ===
Uint32 sw_w = 0, sw_h = 0;
SDL_GPUTexture* swapchain = gpu_ctx_->acquireSwapchainTexture(cmd, &sw_w, &sw_h);
if (swapchain && offscreen_tex_ && offscreen_tex_->isValid()) {
SDL_GPUColorTargetInfo ct = {};
ct.texture = swapchain;
ct.load_op = SDL_GPU_LOADOP_CLEAR;
ct.clear_color = {0.0f, 0.0f, 0.0f, 1.0f};
ct.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPURenderPass* pass2 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
// PostFX: full-screen triangle via vertex_id (no vertex buffer needed)
SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->postfxPipeline());
SDL_GPUTextureSamplerBinding scene_tsb = {offscreen_tex_->texture(), offscreen_tex_->sampler()};
SDL_BindGPUFragmentSamplers(pass2, 0, &scene_tsb, 1);
SDL_DrawGPUPrimitives(pass2, 3, 1, 0, 0);
// UI overlay (alpha-blended, uses sprite pipeline)
if (ui_tex_ && ui_tex_->isValid() && sprite_batch_->overlayIndexCount() > 0) {
SDL_BindGPUGraphicsPipeline(pass2, gpu_pipeline_->spritePipeline());
SDL_GPUBufferBinding vb = {sprite_batch_->vertexBuffer(), 0};
SDL_GPUBufferBinding ib = {sprite_batch_->indexBuffer(), 0};
SDL_BindGPUVertexBuffers(pass2, 0, &vb, 1);
SDL_BindGPUIndexBuffer(pass2, &ib, SDL_GPU_INDEXELEMENTSIZE_32BIT);
SDL_GPUTextureSamplerBinding ui_tsb = {ui_tex_->texture(), ui_tex_->sampler()};
SDL_BindGPUFragmentSamplers(pass2, 0, &ui_tsb, 1);
SDL_DrawGPUIndexedPrimitives(pass2, sprite_batch_->overlayIndexCount(), 1,
sprite_batch_->overlayIndexOffset(), 0, 0);
}
SDL_EndGPURenderPass(pass2);
}
gpu_ctx_->submit(cmd);
}
void Engine::showNotificationForAction(const std::string& text) {
@@ -790,8 +863,8 @@ void Engine::toggleVSync() {
// Actualizar texto en UIManager
ui_manager_->updateVSyncText(vsync_enabled_);
// Aplicar el cambio de V-Sync al renderizador
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
// Aplicar el cambio de V-Sync al contexto GPU
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_);
}
void Engine::toggleFullscreen() {
@@ -837,8 +910,8 @@ void Engine::toggleRealFullscreen() {
SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_);
SDL_SetWindowFullscreen(window_, true);
// Actualizar presentación lógica del renderizador
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Recrear render target offscreen con nueva resolución
recreateOffscreenTexture();
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
@@ -873,8 +946,8 @@ void Engine::toggleRealFullscreen() {
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Restaurar presentación lógica base
SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Recrear render target offscreen con resolución base
recreateOffscreenTexture();
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
@@ -915,81 +988,25 @@ void Engine::toggleIntegerScaling() {
break;
}
// Aplicar el nuevo modo de escalado
SDL_RendererLogicalPresentation presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
// SDL_GPU stretches to fill swapchain by default; just show notification
const char* mode_name = "INTEGER";
switch (current_scaling_mode_) {
case ScalingMode::INTEGER:
presentation = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
mode_name = "INTEGER";
break;
case ScalingMode::LETTERBOX:
presentation = SDL_LOGICAL_PRESENTATION_LETTERBOX;
mode_name = "LETTERBOX";
break;
case ScalingMode::STRETCH:
presentation = SDL_LOGICAL_PRESENTATION_STRETCH;
mode_name = "STRETCH";
break;
case ScalingMode::INTEGER: mode_name = "INTEGER"; break;
case ScalingMode::LETTERBOX: mode_name = "LETTERBOX"; break;
case ScalingMode::STRETCH: mode_name = "STRETCH"; break;
}
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, presentation);
// Mostrar notificación del cambio
std::string notification = std::string("Escalado: ") + mode_name;
ui_manager_->showNotification(notification);
}
void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
int vertex_index = static_cast<int>(batch_vertices_.size());
// Crear 4 vértices para el quad (2 triángulos)
SDL_Vertex vertices[4];
// Convertir colores de int (0-255) a float (0.0-1.0)
float rf = r / 255.0f;
float gf = g / 255.0f;
float bf = b / 255.0f;
// Aplicar escala al tamaño (centrado en el punto x, y)
float scaled_w = w * scale;
float scaled_h = h * scale;
float offset_x = (w - scaled_w) / 2.0f; // Offset para centrar
float offset_y = (h - scaled_h) / 2.0f;
// Vértice superior izquierdo
vertices[0].position = {x + offset_x, y + offset_y};
vertices[0].tex_coord = {0.0f, 0.0f};
vertices[0].color = {rf, gf, bf, 1.0f};
// Vértice superior derecho
vertices[1].position = {x + offset_x + scaled_w, y + offset_y};
vertices[1].tex_coord = {1.0f, 0.0f};
vertices[1].color = {rf, gf, bf, 1.0f};
// Vértice inferior derecho
vertices[2].position = {x + offset_x + scaled_w, y + offset_y + scaled_h};
vertices[2].tex_coord = {1.0f, 1.0f};
vertices[2].color = {rf, gf, bf, 1.0f};
// Vértice inferior izquierdo
vertices[3].position = {x + offset_x, y + offset_y + scaled_h};
vertices[3].tex_coord = {0.0f, 1.0f};
vertices[3].color = {rf, gf, bf, 1.0f};
// Añadir vértices al batch
for (int i = 0; i < 4; i++) {
batch_vertices_.push_back(vertices[i]);
}
// Añadir índices para 2 triángulos
batch_indices_.push_back(vertex_index + 0);
batch_indices_.push_back(vertex_index + 1);
batch_indices_.push_back(vertex_index + 2);
batch_indices_.push_back(vertex_index + 2);
batch_indices_.push_back(vertex_index + 3);
batch_indices_.push_back(vertex_index + 0);
if (!sprite_batch_) return;
sprite_batch_->addSprite(x, y, w, h,
r / 255.0f, g / 255.0f, b / 255.0f, 1.0f,
scale,
static_cast<float>(current_screen_width_),
static_cast<float>(current_screen_height_));
}
// Sistema de zoom dinámico
@@ -1186,7 +1203,7 @@ void Engine::runPerformanceBenchmark() {
}
SDL_HideWindow(window_);
SDL_SetRenderVSync(renderer_, 0);
if (gpu_ctx_) gpu_ctx_->setVSync(false); // Disable VSync for benchmark
const int BENCH_DURATION_MS = 600;
const int WARMUP_FRAMES = 5;
@@ -1194,7 +1211,7 @@ void Engine::runPerformanceBenchmark() {
SimulationMode original_mode = current_mode_;
auto restore = [&]() {
SDL_SetRenderVSync(renderer_, vsync_enabled_ ? 1 : 0);
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_);
SDL_ShowWindow(window_);
current_mode_ = original_mode;
if (shape_manager_->isShapeModeActive()) {
@@ -1261,4 +1278,104 @@ void Engine::runPerformanceBenchmark() {
max_auto_scenario_ = DEMO_AUTO_MIN_SCENARIO;
restore();
}
// ============================================================================
// GPU HELPERS
// ============================================================================
bool Engine::loadGpuSpriteTexture(size_t index) {
if (!gpu_ctx_ || index >= gpu_textures_.size()) return false;
return gpu_textures_[index] && gpu_textures_[index]->isValid();
}
void Engine::recreateOffscreenTexture() {
if (!gpu_ctx_ || !offscreen_tex_) return;
SDL_WaitForGPUIdle(gpu_ctx_->device());
offscreen_tex_->destroy(gpu_ctx_->device());
offscreen_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
// Recreate UI texture to match new screen size
if (ui_tex_) {
ui_tex_->destroy(gpu_ctx_->device());
ui_tex_->createRenderTarget(gpu_ctx_->device(),
current_screen_width_, current_screen_height_,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
}
// Recreate software surface to match new screen size
if (ui_surface_) {
SDL_DestroySurface(ui_surface_);
ui_surface_ = SDL_CreateSurface(current_screen_width_, current_screen_height_,
SDL_PIXELFORMAT_RGBA32);
}
}
void Engine::renderUIToSurface() {
if (!ui_renderer_ || !ui_surface_) return;
// Clear surface (fully transparent)
SDL_SetRenderDrawColor(ui_renderer_, 0, 0, 0, 0);
SDL_RenderClear(ui_renderer_);
// Render UI (HUD, FPS counter, notifications)
ui_manager_->render(ui_renderer_, this, scene_manager_.get(), current_mode_,
state_manager_->getCurrentMode(),
shape_manager_->getActiveShape(), shape_manager_->getConvergence(),
physical_window_width_, physical_window_height_, current_screen_width_);
// Render periodic logo overlay
if (app_logo_) {
app_logo_->render();
}
SDL_RenderPresent(ui_renderer_); // Flush software renderer to surface
}
void Engine::uploadUISurface(SDL_GPUCommandBuffer* cmd_buf) {
if (!ui_tex_ || !ui_tex_->isValid() || !ui_surface_ || !gpu_ctx_) return;
int w = ui_surface_->w;
int h = ui_surface_->h;
Uint32 data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = data_size;
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(gpu_ctx_->device(), &tb_info);
if (!transfer) return;
void* mapped = SDL_MapGPUTransferBuffer(gpu_ctx_->device(), transfer, true);
if (!mapped) {
SDL_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer);
return;
}
memcpy(mapped, ui_surface_->pixels, data_size);
SDL_UnmapGPUTransferBuffer(gpu_ctx_->device(), transfer);
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd_buf);
SDL_GPUTextureTransferInfo src = {};
src.transfer_buffer = transfer;
src.offset = 0;
src.pixels_per_row = static_cast<Uint32>(w);
src.rows_per_layer = static_cast<Uint32>(h);
SDL_GPUTextureRegion dst = {};
dst.texture = ui_tex_->texture();
dst.mip_level = 0;
dst.layer = 0;
dst.x = dst.y = dst.z = 0;
dst.w = static_cast<Uint32>(w);
dst.h = static_cast<Uint32>(h);
dst.d = 1;
SDL_UploadToGPUTexture(copy, &src, &dst, false);
SDL_EndGPUCopyPass(copy);
SDL_ReleaseGPUTransferBuffer(gpu_ctx_->device(), transfer);
}