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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user