Files
vibe3_physics/source/engine.cpp
Sergio Valor e13905567d perf: benchmark CPU-only sin ventana visible durante medición
- Crear ventana con SDL_WINDOW_HIDDEN para que no aparezca hasta que
  el benchmark termine
- runPerformanceBenchmark() elimina todas las llamadas a render() y
  SDL_HideWindow/ShowWindow; mide solo update() (física pura)
- SDL_ShowWindow() se llama en initialize() tras el benchmark y warmup
- Imprimir resultados por consola con formato [Benchmark CPU]

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 01:25:33 +01:00

1601 lines
68 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "engine.hpp"
#include <SDL3/SDL_error.h> // for SDL_GetError
#include <SDL3/SDL_events.h> // for SDL_Event, SDL_PollEvent
#include <SDL3/SDL_init.h> // for SDL_Init, SDL_Quit, SDL_INIT_VIDEO
#include <SDL3/SDL_keycode.h> // for SDL_Keycode
#include <SDL3/SDL_render.h> // for SDL_SetRenderDrawColor, SDL_RenderPresent
#include <SDL3/SDL_timer.h> // for SDL_GetTicks
#include <SDL3/SDL_video.h> // for SDL_CreateWindow, SDL_DestroyWindow, SDL_GetDisplayBounds
#include <algorithm> // for std::min, std::max, std::sort
#include <cmath> // for sqrtf, acosf, cosf, sinf (funciones matemáticas)
#include <cstdlib> // for rand, srand
#include <cstring> // for strlen
#include <ctime> // for time
#include <filesystem> // for path operations
#include <iostream> // for cout
#include <string> // for string
#include "resource_manager.hpp" // for ResourceManager
#ifdef _WIN32
#include <windows.h> // for GetModuleFileName
#endif
#include "ball.hpp" // for Ball
#include "external/mouse.hpp" // for Mouse namespace
#include "external/texture.hpp" // for Texture
#include "shapes/png_shape.hpp" // for PNGShape (dynamic_cast en callbacks LOGO)
// Implementación de métodos públicos
bool Engine::initialize(int width, int height, int zoom, bool fullscreen, AppMode initial_mode) {
bool success = true;
// Obtener resolución de pantalla para validación
if (!SDL_Init(SDL_INIT_VIDEO)) {
std::cout << "¡SDL no se pudo inicializar! Error de SDL: " << SDL_GetError() << std::endl;
return false;
}
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
const auto* dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr;
int screen_w = dm ? dm->w : 1920; // Fallback si falla
int screen_h = dm ? dm->h - WINDOW_DECORATION_HEIGHT : 1080;
if (displays) SDL_free(displays);
// Usar parámetros o valores por defecto
int logical_width = (width > 0) ? width : DEFAULT_SCREEN_WIDTH;
int logical_height = (height > 0) ? height : DEFAULT_SCREEN_HEIGHT;
int window_zoom = (zoom > 0) ? zoom : DEFAULT_WINDOW_ZOOM;
// VALIDACIÓN 1: Si resolución > pantalla → reset a default
if (logical_width > screen_w || logical_height > screen_h) {
std::cout << "Advertencia: Resolución " << logical_width << "x" << logical_height
<< " excede pantalla " << screen_w << "x" << screen_h
<< ". Usando default " << DEFAULT_SCREEN_WIDTH << "x" << DEFAULT_SCREEN_HEIGHT << "\n";
logical_width = DEFAULT_SCREEN_WIDTH;
logical_height = DEFAULT_SCREEN_HEIGHT;
window_zoom = DEFAULT_WINDOW_ZOOM; // Reset zoom también
}
// VALIDACIÓN 2: Calcular max_zoom y ajustar si es necesario
int max_zoom = std::min(screen_w / logical_width, screen_h / logical_height);
if (max_zoom < 1) {
// Resolució lògica no cap en pantalla ni a zoom=1: escalar-la per fer-la càpida
float scale = std::min(static_cast<float>(screen_w) / logical_width,
static_cast<float>(screen_h) / logical_height);
logical_width = std::max(320, static_cast<int>(logical_width * scale));
logical_height = std::max(240, static_cast<int>(logical_height * scale));
window_zoom = 1;
std::cout << "Advertencia: Resolución no cabe en pantalla. Ajustando a "
<< logical_width << "x" << logical_height << "\n";
} else if (window_zoom > max_zoom) {
std::cout << "Advertencia: Zoom " << window_zoom << " excede máximo " << max_zoom
<< " para " << logical_width << "x" << logical_height << ". Ajustando a " << max_zoom << "\n";
window_zoom = max_zoom;
}
// Si se especificaron parámetros CLI y zoom no se especificó, usar zoom=1
if ((width > 0 || height > 0) && zoom == 0) {
window_zoom = 1;
}
// Guardar escala inicial (siempre 1.0 salvo que CLI haya pedido zoom > 1)
current_window_scale_ = static_cast<float>(window_zoom);
// Calcular tamaño de ventana
int window_width = logical_width * window_zoom;
int window_height = logical_height * window_zoom;
// Guardar resolución base (configurada por CLI o default)
base_screen_width_ = logical_width;
base_screen_height_ = logical_height;
current_screen_width_ = logical_width;
current_screen_height_ = logical_height;
// SDL ya inicializado arriba para validación
{
// Crear ventana principal (fullscreen si se especifica)
// 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 = SDL_WINDOW_HIDDEN; // Oculta hasta que el benchmark termine
if (fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN;
}
window_ = SDL_CreateWindow(WINDOW_CAPTION, window_width, window_height, window_flags);
if (window_ == nullptr) {
std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << std::endl;
success = false;
} else {
// Centrar ventana en pantalla si no está en fullscreen
if (!fullscreen) {
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// 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 {
gpu_ctx_->setVSync(vsync_enabled_);
// 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
}
}
}
}
// Inicializar otros componentes si SDL se inicializó correctamente
if (success) {
// Cargar todas las texturas disponibles desde data/balls/
std::string resources_dir = getResourcesDirectory();
std::string balls_dir = resources_dir + "/data/balls";
struct TextureInfo {
std::string name;
std::shared_ptr<Texture> texture; // legacy (para physics sizing)
std::string path; // resource path para GPU upload
int width;
};
std::vector<TextureInfo> texture_files;
// Buscar todas las texturas PNG en data/balls/
namespace fs = std::filesystem;
if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) {
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 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, fullpath, width});
}
}
} else {
// Fallback: cargar texturas desde pack
if (ResourceManager::isPackLoaded()) {
auto pack_resources = ResourceManager::getResourceList();
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);
std::string name = tex_name.substr(0, tex_name.find('.'));
auto texture = std::make_shared<Texture>(ui_renderer_, resource);
int width = texture->getWidth();
texture_files.push_back({name, texture, resource, width});
}
}
}
}
// 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;
});
// 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
if (textures_.empty()) {
std::cerr << "ERROR: No se pudieron cargar texturas" << std::endl;
success = false;
}
// 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;
break;
}
}
texture_ = textures_[current_texture_index_];
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;
}
// Capacidad = número de bolas del escenario máximo (o custom si es mayor).
// addBackground() no usa el guard de pushQuad(), así que no consume slots aquí.
// init() reserva internamente +1 quad extra garantizado para el overlay.
int sprite_capacity = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO];
if (custom_scenario_enabled_ && custom_scenario_balls_ > sprite_capacity)
sprite_capacity = custom_scenario_balls_;
sprite_batch_ = std::make_unique<GpuSpriteBatch>();
if (!sprite_batch_->init(gpu_ctx_->device(), sprite_capacity)) {
std::cerr << "ERROR: No se pudo crear el sprite batch GPU" << std::endl;
success = false;
}
gpu_ball_buffer_ = std::make_unique<GpuBallBuffer>();
if (!gpu_ball_buffer_->init(gpu_ctx_->device())) {
std::cerr << "ERROR: No se pudo crear el ball buffer GPU" << std::endl;
success = false;
}
ball_gpu_data_.reserve(GpuBallBuffer::MAX_BALLS);
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)));
// Inicializar InputHandler (sin estado)
input_handler_ = std::make_unique<InputHandler>();
// Inicializar ThemeManager PRIMERO (requerido por Notifier y SceneManager)
theme_manager_ = std::make_unique<ThemeManager>();
theme_manager_->initialize();
{
int max_balls = BALL_COUNT_SCENARIOS[DEMO_AUTO_MAX_SCENARIO];
if (custom_scenario_enabled_ && custom_scenario_balls_ > max_balls)
max_balls = custom_scenario_balls_;
theme_manager_->setMaxBallCount(max_balls);
}
// Inicializar SceneManager (gestión de bolas y física)
scene_manager_ = std::make_unique<SceneManager>(current_screen_width_, current_screen_height_);
scene_manager_->initialize(0, texture_, theme_manager_.get()); // Escenario 0 (10 bolas) por defecto
// Propagar configuración custom si fue establecida antes de initialize()
if (custom_scenario_enabled_)
scene_manager_->setCustomBallCount(custom_scenario_balls_);
// Calcular tamaño físico de ventana ANTES de inicializar UIManager
// NOTA: No llamar a updatePhysicalWindowSize() aquí porque ui_manager_ aún no existe
// Calcular manualmente para poder pasar valores al constructor de UIManager
int window_w = 0, window_h = 0;
SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
physical_window_width_ = window_w;
physical_window_height_ = window_h;
// 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(ui_renderer_, theme_manager_.get(),
physical_window_width_, physical_window_height_,
current_screen_width_, current_screen_height_);
// Inicializar ShapeManager (gestión de figuras 3D)
shape_manager_ = std::make_unique<ShapeManager>();
shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), nullptr,
current_screen_width_, current_screen_height_);
// Inicializar StateManager (gestión de estados DEMO/LOGO)
state_manager_ = std::make_unique<StateManager>();
state_manager_->initialize(this, scene_manager_.get(), theme_manager_.get(), shape_manager_.get());
// Establecer modo inicial si no es SANDBOX (default)
// Usar métodos de alto nivel que ejecutan las acciones de configuración
if (initial_mode == AppMode::DEMO) {
state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_);
// Como estamos en SANDBOX (default), toggleDemoMode() cambiará a DEMO + randomizará
}
else if (initial_mode == AppMode::DEMO_LITE) {
state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_);
// Como estamos en SANDBOX (default), toggleDemoLiteMode() cambiará a DEMO_LITE + randomizará
}
else if (initial_mode == AppMode::LOGO) {
size_t initial_ball_count = scene_manager_->getBallCount();
state_manager_->enterLogoMode(false, current_screen_width_, current_screen_height_, initial_ball_count);
// enterLogoMode() hace: setState(LOGO) + configuración visual completa
}
// Actualizar ShapeManager con StateManager (dependencia circular - StateManager debe existir primero)
shape_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(),
current_screen_width_, current_screen_height_);
// Inicializar BoidManager (gestión de comportamiento de enjambre)
boid_manager_ = std::make_unique<BoidManager>();
boid_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(),
current_screen_width_, current_screen_height_);
// Inicializar AppLogo (logo periódico en pantalla)
app_logo_ = std::make_unique<AppLogo>();
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();
}
// Benchmark de rendimiento (determina max_auto_scenario_ para modos automáticos)
if (!skip_benchmark_)
runPerformanceBenchmark();
else if (custom_scenario_enabled_)
custom_auto_available_ = true; // benchmark omitido: confiar en que el hardware lo soporta
// Precalentar caché: shapes PNG (evitar I/O en primera activación de PNG_SHAPE)
{
unsigned char* tmp = nullptr; size_t tmp_size = 0;
ResourceManager::loadResource("shapes/jailgames.png", tmp, tmp_size);
delete[] tmp;
}
// Mostrar ventana ahora que el benchmark terminó
SDL_ShowWindow(window_);
}
return success;
}
void Engine::run() {
while (!should_exit_) {
calculateDeltaTime();
// Procesar eventos de entrada (teclado, ratón, ventana)
if (input_handler_->processEvents(*this)) {
should_exit_ = true;
}
update();
render();
}
}
void Engine::shutdown() {
// 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_ball_buffer_) { gpu_ball_buffer_->destroy(gpu_ctx_->device()); gpu_ball_buffer_.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;
}
SDL_Quit();
}
// Métodos privados - esqueleto básico por ahora
void Engine::calculateDeltaTime() {
Uint64 current_time = SDL_GetTicks();
// En el primer frame, inicializar el tiempo anterior
if (last_frame_time_ == 0) {
last_frame_time_ = current_time;
delta_time_ = 1.0f / 60.0f; // Asumir 60 FPS para el primer frame
return;
}
// Calcular delta time en segundos
delta_time_ = (current_time - last_frame_time_) / 1000.0f;
last_frame_time_ = current_time;
// Limitar delta time para evitar saltos grandes (pausa larga, depuración, etc.)
if (delta_time_ > 0.05f) { // Máximo 50ms (20 FPS mínimo)
delta_time_ = 1.0f / 60.0f; // Fallback a 60 FPS
}
}
void Engine::update() {
// Accumulate time for PostFX uniforms
postfx_uniforms_.screen_height = static_cast<float>(current_screen_height_);
// Actualizar visibilidad del cursor (auto-ocultar tras inactividad)
Mouse::updateCursorVisibility();
// Obtener tiempo actual
Uint64 current_time = SDL_GetTicks();
// Actualizar UI (FPS, notificaciones, texto obsoleto) - delegado a UIManager
ui_manager_->update(current_time, delta_time_);
// Bifurcar actualización según modo activo
if (current_mode_ == SimulationMode::PHYSICS) {
// Modo física normal: actualizar física de cada pelota (delegado a SceneManager)
scene_manager_->update(delta_time_);
} else if (current_mode_ == SimulationMode::SHAPE) {
// Modo Figura 3D: actualizar figura polimórfica
updateShape();
} else if (current_mode_ == SimulationMode::BOIDS) {
// CPU boids: actualizar comportamiento de enjambre (delegado a BoidManager)
boid_manager_->update(delta_time_);
}
// Actualizar Modo DEMO/LOGO (delegado a StateManager)
state_manager_->update(delta_time_, shape_manager_->getConvergence(), shape_manager_->getActiveShape());
// Actualizar transiciones de temas (delegado a ThemeManager)
theme_manager_->update(delta_time_);
// Actualizar AppLogo (logo periódico)
if (app_logo_) {
app_logo_->update(delta_time_, state_manager_->getCurrentMode());
}
}
// === IMPLEMENTACIÓN DE MÉTODOS PÚBLICOS PARA INPUT HANDLER ===
// Gravedad y física
void Engine::handleGravityToggle() {
// Si estamos en modo boids, salir a modo física CON GRAVEDAD OFF
// Según RULES.md: "BOIDS a PHYSICS: Pulsando la tecla G: Gravedad OFF"
if (current_mode_ == SimulationMode::BOIDS) {
toggleBoidsMode(false); // Cambiar a PHYSICS sin activar gravedad (preserva inercia)
// NO llamar a forceBallsGravityOff() porque aplica impulsos que destruyen la inercia de BOIDS
// La gravedad ya está desactivada por BoidManager::activateBoids() y se mantiene al salir
showNotificationForAction("Modo física, gravedad off");
return;
}
// Si estamos en modo figura, salir a modo física SIN GRAVEDAD
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeModeInternal(false); // Desactivar figura sin forzar gravedad ON
showNotificationForAction("Gravedad off");
} else {
scene_manager_->switchBallsGravity(); // Toggle normal en modo física
// Determinar estado actual de gravedad (gravity_force_ != 0.0f significa ON)
const Ball* first_ball = scene_manager_->getFirstBall();
bool gravity_on = (first_ball == nullptr) ? true : (first_ball->getGravityForce() != 0.0f);
showNotificationForAction(gravity_on ? "Gravedad on" : "Gravedad off");
}
}
void Engine::handleGravityDirectionChange(GravityDirection direction, const char* notification_text) {
// Si estamos en modo boids, salir a modo física primero PRESERVANDO VELOCIDAD
if (current_mode_ == SimulationMode::BOIDS) {
current_mode_ = SimulationMode::PHYSICS;
boid_manager_->deactivateBoids(false); // NO activar gravedad aún (preservar momentum)
scene_manager_->forceBallsGravityOn(); // Activar gravedad SIN impulsos (preserva velocidad)
}
// Si estamos en modo figura, salir a modo física CON gravedad
else if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeModeInternal(); // Desactivar figura (activa gravedad automáticamente)
} else {
scene_manager_->enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
scene_manager_->changeGravityDirection(direction);
showNotificationForAction(notification_text);
}
// Display y depuración
void Engine::toggleDebug() {
ui_manager_->toggleDebug();
}
void Engine::toggleHelp() {
ui_manager_->toggleHelp();
}
// Figuras 3D
void Engine::toggleShapeMode() {
toggleShapeModeInternal();
// Mostrar notificación según el modo actual después del toggle
if (current_mode_ == SimulationMode::PHYSICS) {
showNotificationForAction("Modo Física");
} else {
// Mostrar nombre de la figura actual (orden debe coincidir con enum ShapeType)
// Índices: 0=NONE, 1=SPHERE, 2=CUBE, 3=HELIX, 4=TORUS, 5=LISSAJOUS, 6=CYLINDER, 7=ICOSAHEDRON, 8=ATOM, 9=PNG_SHAPE
const char* shape_names[] = {"Ninguna", "Esfera", "Cubo", "Hélice", "Toroide", "Lissajous", "Cilindro", "Icosaedro", "Átomo", "Forma PNG"};
showNotificationForAction(shape_names[static_cast<int>(shape_manager_->getCurrentShapeType())]);
}
}
void Engine::activateShape(ShapeType type, const char* notification_text) {
activateShapeInternal(type);
showNotificationForAction(notification_text);
}
void Engine::handleShapeScaleChange(bool increase) {
// Delegar a ShapeManager (gestiona escala y muestra notificación en SANDBOX)
shape_manager_->handleShapeScaleChange(increase);
}
void Engine::resetShapeScale() {
// Delegar a ShapeManager (resetea escala y muestra notificación en SANDBOX)
shape_manager_->resetShapeScale();
}
void Engine::toggleDepthZoom() {
// Delegar a ShapeManager (toggle depth zoom y muestra notificación en SANDBOX)
shape_manager_->toggleDepthZoom();
}
// Boids (comportamiento de enjambre)
bool Engine::isScenarioAllowedForBoids(int scenario_id) const {
int ball_count = (scenario_id == CUSTOM_SCENARIO_IDX)
? custom_scenario_balls_
: BALL_COUNT_SCENARIOS[scenario_id];
return ball_count <= BOIDS_MAX_BALLS;
}
void Engine::toggleBoidsMode(bool force_gravity_on) {
if (current_mode_ != SimulationMode::BOIDS) {
// Intentando activar BOIDS — verificar escenario actual
if (!isScenarioAllowedForBoids(scene_manager_->getCurrentScenario())) {
showNotificationForAction("Boids: máximo 1.000 pelotas");
return;
}
}
if (current_mode_ == SimulationMode::BOIDS) {
// Salir del modo boids
current_mode_ = SimulationMode::PHYSICS;
boid_manager_->deactivateBoids(force_gravity_on);
} else {
// Entrar al modo boids (desde PHYSICS o SHAPE)
if (current_mode_ == SimulationMode::SHAPE) {
shape_manager_->toggleShapeMode(false);
current_mode_ = SimulationMode::PHYSICS;
}
// Activar modo boids en CPU (configura gravedad OFF, inicializa velocidades)
current_mode_ = SimulationMode::BOIDS;
boid_manager_->activateBoids();
}
}
// Temas de colores
void Engine::cycleTheme(bool forward) {
if (forward) {
theme_manager_->cycleTheme();
} else {
theme_manager_->cyclePrevTheme();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
void Engine::switchThemeByNumpad(int numpad_key) {
// Mapear tecla numpad a índice de tema según página actual
int theme_index = -1;
if (theme_page_ == 0) {
// Página 0: Temas 0-9 (estáticos + SUNRISE)
if (numpad_key >= 0 && numpad_key <= 9) {
theme_index = (numpad_key == 0) ? 9 : (numpad_key - 1);
}
} else {
// Página 1: Temas 10-14 (dinámicos)
if (numpad_key >= 1 && numpad_key <= 5) {
theme_index = 9 + numpad_key;
}
}
if (theme_index != -1) {
theme_manager_->switchToTheme(theme_index);
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
}
void Engine::toggleThemePage() {
theme_page_ = (theme_page_ == 0) ? 1 : 0;
showNotificationForAction((theme_page_ == 0) ? "Página 1" : "Página 2");
}
void Engine::pauseDynamicTheme() {
theme_manager_->pauseDynamic();
}
// Sprites/Texturas
void Engine::switchTexture() {
switchTextureInternal(true); // Mostrar notificación en modo manual
}
// Control manual del benchmark (--skip-benchmark, --max-balls)
void Engine::setSkipBenchmark() {
skip_benchmark_ = true;
}
void Engine::setMaxBallsOverride(int n) {
skip_benchmark_ = true;
int best = DEMO_AUTO_MIN_SCENARIO;
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= DEMO_AUTO_MAX_SCENARIO; ++i) {
if (BALL_COUNT_SCENARIOS[i] <= n) best = i;
else break;
}
max_auto_scenario_ = best;
}
// Escenario custom (--custom-balls)
void Engine::setCustomScenario(int balls) {
custom_scenario_balls_ = balls;
custom_scenario_enabled_ = true;
// scene_manager_ puede no existir aún (llamada pre-init); propagación en initialize()
if (scene_manager_)
scene_manager_->setCustomBallCount(balls);
}
// Escenarios (número de pelotas)
void Engine::changeScenario(int scenario_id, const char* notification_text) {
if (current_mode_ == SimulationMode::BOIDS) {
if (!isScenarioAllowedForBoids(scenario_id)) {
showNotificationForAction("Boids: máximo 1.000 pelotas");
return;
}
}
// Pasar el modo actual al SceneManager para inicialización correcta
scene_manager_->changeScenario(scenario_id, current_mode_);
// Si estamos en modo SHAPE, regenerar la figura con nuevo número de pelotas
if (current_mode_ == SimulationMode::SHAPE) {
generateShape();
scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario
}
// Si estamos en modo BOIDS, desactivar gravedad (modo BOIDS = gravedad OFF siempre)
if (current_mode_ == SimulationMode::BOIDS) {
scene_manager_->forceBallsGravityOff();
}
showNotificationForAction(notification_text);
}
// Zoom y fullscreen
void Engine::handleZoomIn() {
if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
zoomIn();
}
}
void Engine::handleZoomOut() {
if (!fullscreen_enabled_ && !real_fullscreen_enabled_) {
zoomOut();
}
}
// Modos de aplicación (DEMO/LOGO) - Delegados a StateManager
void Engine::toggleDemoMode() {
AppMode prev_mode = state_manager_->getCurrentMode();
state_manager_->toggleDemoMode(current_screen_width_, current_screen_height_);
AppMode new_mode = state_manager_->getCurrentMode();
// Mostrar notificación según el modo resultante
if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
showNotificationForAction("Modo sandbox");
} else if (new_mode == AppMode::DEMO && prev_mode != AppMode::DEMO) {
showNotificationForAction("Modo demo");
}
}
void Engine::toggleDemoLiteMode() {
AppMode prev_mode = state_manager_->getCurrentMode();
state_manager_->toggleDemoLiteMode(current_screen_width_, current_screen_height_);
AppMode new_mode = state_manager_->getCurrentMode();
// Mostrar notificación según el modo resultante
if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
showNotificationForAction("Modo sandbox");
} else if (new_mode == AppMode::DEMO_LITE && prev_mode != AppMode::DEMO_LITE) {
showNotificationForAction("Modo demo lite");
}
}
void Engine::toggleLogoMode() {
AppMode prev_mode = state_manager_->getCurrentMode();
state_manager_->toggleLogoMode(current_screen_width_, current_screen_height_, scene_manager_->getBallCount());
AppMode new_mode = state_manager_->getCurrentMode();
// Mostrar notificación según el modo resultante
if (new_mode == AppMode::SANDBOX && prev_mode != AppMode::SANDBOX) {
showNotificationForAction("Modo sandbox");
} else if (new_mode == AppMode::LOGO && prev_mode != AppMode::LOGO) {
showNotificationForAction("Modo logo");
}
}
void Engine::render() {
if (!gpu_ctx_ || !sprite_batch_ || !gpu_pipeline_) return;
// === Render UI text to software surface ===
renderUIToSurface();
// === Acquire command buffer ===
SDL_GPUCommandBuffer* cmd = gpu_ctx_->acquireCommandBuffer();
if (!cmd) return;
// === Upload UI surface to GPU texture (inline copy pass) ===
uploadUISurface(cmd);
// === Build sprite batch ===
sprite_batch_->beginFrame();
// 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);
// Sprites (balls)
const auto& balls = scene_manager_->getBalls();
const float sw = static_cast<float>(current_screen_width_);
const float sh = static_cast<float>(current_screen_height_);
if (current_mode_ == SimulationMode::SHAPE) {
// SHAPE mode: bucket sort by depth Z (Painter's Algorithm), with depth scale.
// Uses the sprite batch (supports per-sprite scale, needed for depth zoom).
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);
}
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);
float brightness = balls[idx]->getDepthBrightness();
float depth_scale = balls[idx]->getDepthScale();
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, sw, sh);
}
depth_buckets_[b].clear();
}
} else {
// PHYSICS / CPU-BOIDS mode: build instanced ball buffer (GPU instanced rendering).
// 32 bytes per ball instead of 4×32 bytes per quad — 4× less upload bandwidth.
ball_gpu_data_.clear();
for (size_t idx = 0; idx < balls.size(); idx++) {
SDL_FRect pos = balls[idx]->getPosition();
Color color = theme_manager_->getInterpolatedColor(idx);
// Convert to NDC center + NDC half-size (both positive)
float cx = ((pos.x + pos.w * 0.5f) / sw) * 2.0f - 1.0f;
float cy = 1.0f - ((pos.y + pos.h * 0.5f) / sh) * 2.0f;
float hw = pos.w / sw;
float hh = pos.h / sh;
ball_gpu_data_.push_back({cx, cy, hw, hh,
color.r / 255.0f, color.g / 255.0f,
color.b / 255.0f, 1.0f});
}
}
// UI overlay quad (drawn in Pass 2 over the postfx output)
sprite_batch_->addFullscreenOverlay();
// Upload sprite batch (background + SHAPE balls + UI overlay quad)
if (!sprite_batch_->uploadBatch(gpu_ctx_->device(), cmd)) {
gpu_ctx_->submit(cmd);
return;
}
// Upload instanced ball buffer (PHYSICS / CPU-BOIDS modes)
bool use_instanced_balls = (current_mode_ != SimulationMode::SHAPE) && !ball_gpu_data_.empty();
if (use_instanced_balls) {
gpu_ball_buffer_->upload(gpu_ctx_->device(), cmd,
ball_gpu_data_.data(), static_cast<int>(ball_gpu_data_.size()));
}
GpuTexture* sprite_tex = (!gpu_textures_.empty())
? gpu_textures_[current_texture_index_].get() : nullptr;
// === Pass 1: Render background + balls 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;
SDL_GPURenderPass* pass1 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
// Background (white texture tinted by vertex color, via sprite batch)
SDL_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->spritePipeline());
{
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);
}
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);
}
if (use_instanced_balls && gpu_ball_buffer_->count() > 0) {
// PHYSICS / CPU-BOIDS: instanced rendering — 6 procedural vertices per instance
SDL_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->ballPipeline());
SDL_GPUBufferBinding ball_vb = {gpu_ball_buffer_->buffer(), 0};
SDL_BindGPUVertexBuffers(pass1, 0, &ball_vb, 1);
SDL_GPUTextureSamplerBinding tsb = {sprite_tex->texture(), sprite_tex->sampler()};
SDL_BindGPUFragmentSamplers(pass1, 0, &tsb, 1);
SDL_DrawGPUPrimitives(pass1, 6, static_cast<Uint32>(gpu_ball_buffer_->count()), 0, 0);
} else if (!use_instanced_balls && sprite_batch_->spriteIndexCount() > 0) {
// SHAPE: sprite batch with depth sort (re-bind sprite pipeline + buffers)
SDL_BindGPUGraphicsPipeline(pass1, gpu_pipeline_->spritePipeline());
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);
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);
}
SDL_EndGPURenderPass(pass1);
}
// === Pass 2+: External multi-pass shader OR native PostFX → 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()) {
// Helper lambda for viewport/scissor (used in the final pass)
auto applyViewport = [&](SDL_GPURenderPass* rp) {
if (!fullscreen_enabled_) return;
float vp_x, vp_y, vp_w, vp_h;
if (current_scaling_mode_ == ScalingMode::STRETCH) {
vp_x = 0.0f; vp_y = 0.0f;
vp_w = static_cast<float>(sw_w);
vp_h = static_cast<float>(sw_h);
} else if (current_scaling_mode_ == ScalingMode::INTEGER) {
int scale = static_cast<int>(std::min(sw_w / static_cast<Uint32>(base_screen_width_),
sw_h / static_cast<Uint32>(base_screen_height_)));
if (scale < 1) scale = 1;
vp_w = static_cast<float>(base_screen_width_ * scale);
vp_h = static_cast<float>(base_screen_height_ * scale);
vp_x = (static_cast<float>(sw_w) - vp_w) * 0.5f;
vp_y = (static_cast<float>(sw_h) - vp_h) * 0.5f;
} else { // LETTERBOX
float scale = std::min(static_cast<float>(sw_w) / base_screen_width_,
static_cast<float>(sw_h) / base_screen_height_);
vp_w = base_screen_width_ * scale;
vp_h = base_screen_height_ * scale;
vp_x = (static_cast<float>(sw_w) - vp_w) * 0.5f;
vp_y = (static_cast<float>(sw_h) - vp_h) * 0.5f;
}
SDL_GPUViewport vp = {vp_x, vp_y, vp_w, vp_h, 0.0f, 1.0f};
SDL_SetGPUViewport(rp, &vp);
SDL_Rect scissor = {static_cast<int>(vp_x), static_cast<int>(vp_y),
static_cast<int>(vp_w), static_cast<int>(vp_h)};
SDL_SetGPUScissor(rp, &scissor);
};
{
// --- Native PostFX path ---
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);
applyViewport(pass2);
// 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_PushGPUFragmentUniformData(cmd, 0, &postfx_uniforms_, sizeof(PostFXUniforms));
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);
} // end native PostFX
} // end if (swapchain && ...)
gpu_ctx_->submit(cmd);
}
void Engine::showNotificationForAction(const std::string& text) {
// IMPORTANTE: Esta función solo se llama desde handlers de teclado (acciones manuales)
// NUNCA se llama desde código automático (DEMO/LOGO), por lo tanto siempre mostramos notificación
// Delegar a UIManager
ui_manager_->showNotification(text, NOTIFICATION_DURATION);
}
void Engine::pushBallsAwayFromGravity() {
scene_manager_->pushBallsAwayFromGravity();
}
void Engine::toggleVSync() {
vsync_enabled_ = !vsync_enabled_;
// Actualizar texto en UIManager
ui_manager_->updateVSyncText(vsync_enabled_);
// Aplicar el cambio de V-Sync al contexto GPU
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_);
}
void Engine::toggleFullscreen() {
// Si está en modo real fullscreen, primero salir de él
if (real_fullscreen_enabled_) {
toggleRealFullscreen(); // Esto lo desactiva
}
fullscreen_enabled_ = !fullscreen_enabled_;
SDL_SetWindowFullscreen(window_, fullscreen_enabled_);
// Si acabamos de salir de fullscreen, restaurar tamaño de ventana
if (!fullscreen_enabled_) {
int restore_w = static_cast<int>(std::round(base_screen_width_ * current_window_scale_));
int restore_h = static_cast<int>(std::round(base_screen_height_ * current_window_scale_));
SDL_SetWindowSize(window_, restore_w, restore_h);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Actualizar dimensiones físicas después del cambio
updatePhysicalWindowSize();
}
void Engine::toggleRealFullscreen() {
// Si está en modo fullscreen normal, primero desactivarlo
if (fullscreen_enabled_) {
fullscreen_enabled_ = false;
SDL_SetWindowFullscreen(window_, false);
}
real_fullscreen_enabled_ = !real_fullscreen_enabled_;
if (real_fullscreen_enabled_) {
// Obtener resolución del escritorio
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays != nullptr && num_displays > 0) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm != nullptr) {
// Cambiar a resolución nativa del escritorio
current_screen_width_ = dm->w;
current_screen_height_ = dm->h;
// Recrear ventana con nueva resolución
SDL_SetWindowSize(window_, current_screen_width_, current_screen_height_);
SDL_SetWindowFullscreen(window_, true);
// Recrear render target offscreen con nueva resolución
recreateOffscreenTexture();
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
// Reinicar la escena con nueva resolución
scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
// Actualizar tamaño de pantalla para boids (wrapping boundaries)
boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
// Actualizar ShapeManager con nueva resolución (siempre, independientemente del modo actual)
shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
// Actualizar AppLogo con nueva resolución
if (app_logo_) {
app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
}
// Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones
if (current_mode_ == SimulationMode::SHAPE) {
generateShape(); // Regenerar figura con nuevas dimensiones de pantalla
scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario
}
}
SDL_free(displays);
}
} else {
// Volver a resolución base (configurada por CLI o default)
current_screen_width_ = base_screen_width_;
current_screen_height_ = base_screen_height_;
current_field_scale_ = 1.0f; // Resetear escala de campo al salir de fullscreen real
// Restaurar ventana normal con la escala actual
SDL_SetWindowFullscreen(window_, false);
int restore_w = static_cast<int>(std::round(base_screen_width_ * current_window_scale_));
int restore_h = static_cast<int>(std::round(base_screen_height_ * current_window_scale_));
SDL_SetWindowSize(window_, restore_w, restore_h);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Recrear render target offscreen con resolución base
recreateOffscreenTexture();
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
// Reinicar la escena con resolución original
scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
// Actualizar ShapeManager con resolución restaurada (siempre, independientemente del modo actual)
shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
// Actualizar AppLogo con resolución restaurada
if (app_logo_) {
app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
}
// Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones
if (current_mode_ == SimulationMode::SHAPE) {
generateShape(); // Regenerar figura con nuevas dimensiones de pantalla
scene_manager_->enableShapeAttractionAll(true); // Crítico tras changeScenario
}
}
}
void Engine::applyPostFXPreset(int mode) {
static constexpr float presets[4][3] = {
{0.8f, 0.0f, 0.0f}, // 0: Vinyeta
{0.8f, 0.0f, 0.8f}, // 1: Scanlines
{0.8f, 0.2f, 0.0f}, // 2: Cromàtica
{0.8f, 0.2f, 0.8f}, // 3: Complet
};
postfx_uniforms_.vignette_strength = presets[mode][0];
postfx_uniforms_.chroma_strength = presets[mode][1];
postfx_uniforms_.scanline_strength = presets[mode][2];
// Reaplicar overrides de CLI si están activos
if (postfx_override_vignette_ >= 0.f) postfx_uniforms_.vignette_strength = postfx_override_vignette_;
if (postfx_override_chroma_ >= 0.f) postfx_uniforms_.chroma_strength = postfx_override_chroma_;
}
void Engine::handlePostFXCycle() {
cycleShader();
}
void Engine::handlePostFXToggle() {
static constexpr const char* names[4] = {
"PostFX viñeta", "PostFX scanlines",
"PostFX cromática", "PostFX completo"
};
postfx_enabled_ = !postfx_enabled_;
if (postfx_enabled_) {
applyPostFXPreset(postfx_effect_mode_);
showNotificationForAction(names[postfx_effect_mode_]);
} else {
postfx_uniforms_.vignette_strength = 0.0f;
postfx_uniforms_.chroma_strength = 0.0f;
postfx_uniforms_.scanline_strength = 0.0f;
showNotificationForAction("PostFX desactivado");
}
}
void Engine::setInitialPostFX(int mode) {
postfx_effect_mode_ = mode;
postfx_enabled_ = true;
applyPostFXPreset(mode);
}
void Engine::setPostFXParamOverrides(float vignette, float chroma) {
postfx_override_vignette_ = vignette;
postfx_override_chroma_ = chroma;
postfx_enabled_ = true;
// Aplicar inmediatamente sobre el preset activo
if (vignette >= 0.f) postfx_uniforms_.vignette_strength = vignette;
if (chroma >= 0.f) postfx_uniforms_.chroma_strength = chroma;
}
void Engine::cycleShader() {
// X no hace nada si PostFX está desactivado
if (!postfx_enabled_) return;
// Cicla solo entre los 4 modos (sin OFF)
postfx_effect_mode_ = (postfx_effect_mode_ + 1) % 4;
applyPostFXPreset(postfx_effect_mode_);
static constexpr const char* names[4] = {
"PostFX viñeta", "PostFX scanlines",
"PostFX cromática", "PostFX completo"
};
showNotificationForAction(names[postfx_effect_mode_]);
}
void Engine::toggleIntegerScaling() {
// Ciclar entre los 3 modos: INTEGER → LETTERBOX → STRETCH → INTEGER
switch (current_scaling_mode_) {
case ScalingMode::INTEGER:
current_scaling_mode_ = ScalingMode::LETTERBOX;
break;
case ScalingMode::LETTERBOX:
current_scaling_mode_ = ScalingMode::STRETCH;
break;
case ScalingMode::STRETCH:
current_scaling_mode_ = ScalingMode::INTEGER;
break;
}
const char* mode_name = "entero";
switch (current_scaling_mode_) {
case ScalingMode::INTEGER: mode_name = "entero"; break;
case ScalingMode::LETTERBOX: mode_name = "letterbox"; break;
case ScalingMode::STRETCH: mode_name = "stretch"; break;
}
showNotificationForAction(std::string("Escalado ") + mode_name);
}
void Engine::addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale) {
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 escala de ventana (pasos del 10%)
float Engine::calculateMaxWindowScale() const {
SDL_Rect bounds;
if (!SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) { // bool: false = error
return WINDOW_SCALE_MIN; // Fallback solo si falla de verdad
}
float max_by_w = static_cast<float>(bounds.w - 2 * WINDOW_DESKTOP_MARGIN) / base_screen_width_;
float max_by_h = static_cast<float>(bounds.h - 2 * WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT) / base_screen_height_;
float result = std::max(WINDOW_SCALE_MIN, std::min(max_by_w, max_by_h));
return result;
}
// Redimensiona la ventana física manteniéndo su centro, con clamping a pantalla.
static void resizeWindowCentered(SDL_Window* window, int new_w, int new_h) {
int cur_x, cur_y, cur_w, cur_h;
SDL_GetWindowPosition(window, &cur_x, &cur_y);
SDL_GetWindowSize(window, &cur_w, &cur_h);
int new_x = cur_x + (cur_w - new_w) / 2;
int new_y = cur_y + (cur_h - new_h) / 2;
SDL_Rect bounds;
if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &bounds)) {
new_x = std::max(WINDOW_DESKTOP_MARGIN,
std::min(new_x, bounds.w - new_w - WINDOW_DESKTOP_MARGIN));
new_y = std::max(WINDOW_DESKTOP_MARGIN,
std::min(new_y, bounds.h - new_h - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT));
}
SDL_SetWindowSize(window, new_w, new_h);
SDL_SetWindowPosition(window, new_x, new_y);
}
void Engine::setWindowScale(float new_scale) {
float max_scale = calculateMaxWindowScale();
new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale));
new_scale = std::round(new_scale * 10.0f) / 10.0f;
if (new_scale == current_window_scale_) return;
int new_width = static_cast<int>(std::round(current_screen_width_ * new_scale));
int new_height = static_cast<int>(std::round(current_screen_height_ * new_scale));
resizeWindowCentered(window_, new_width, new_height);
current_window_scale_ = new_scale;
updatePhysicalWindowSize();
}
void Engine::zoomIn() {
float prev = current_window_scale_;
setWindowScale(current_window_scale_ + WINDOW_SCALE_STEP);
if (current_window_scale_ != prev) {
char buf[32];
std::snprintf(buf, sizeof(buf), "Zoom %.0f%%", current_window_scale_ * 100.0f);
showNotificationForAction(buf);
}
}
void Engine::zoomOut() {
float prev = current_window_scale_;
setWindowScale(current_window_scale_ - WINDOW_SCALE_STEP);
if (current_window_scale_ != prev) {
char buf[32];
std::snprintf(buf, sizeof(buf), "Zoom %.0f%%", current_window_scale_ * 100.0f);
showNotificationForAction(buf);
}
}
void Engine::setFieldScale(float new_scale) {
float max_scale = calculateMaxWindowScale();
new_scale = std::max(WINDOW_SCALE_MIN, std::min(new_scale, max_scale));
new_scale = std::round(new_scale * 10.0f) / 10.0f;
if (new_scale == current_field_scale_) return;
current_field_scale_ = new_scale;
current_screen_width_ = static_cast<int>(std::round(base_screen_width_ * new_scale));
current_screen_height_ = static_cast<int>(std::round(base_screen_height_ * new_scale));
// Ajustar ventana física: campo lógico × zoom actual, manteniendo centro
int phys_w = static_cast<int>(std::round(current_screen_width_ * current_window_scale_));
int phys_h = static_cast<int>(std::round(current_screen_height_ * current_window_scale_));
resizeWindowCentered(window_, phys_w, phys_h);
// Recrear render target con nueva resolución lógica
recreateOffscreenTexture();
updatePhysicalWindowSize();
// Reiniciar escena (igual que F4)
scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
shape_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
if (app_logo_) app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
if (current_mode_ == SimulationMode::SHAPE) {
generateShape();
scene_manager_->enableShapeAttractionAll(true);
}
showNotificationForAction("Campo " + std::to_string(current_screen_width_) +
" x " + std::to_string(current_screen_height_));
}
void Engine::fieldSizeUp() { setFieldScale(current_field_scale_ + WINDOW_SCALE_STEP); }
void Engine::fieldSizeDown() { setFieldScale(current_field_scale_ - WINDOW_SCALE_STEP); }
void Engine::updatePhysicalWindowSize() {
if (real_fullscreen_enabled_) {
// En fullscreen real (F4), usar resolución del display
physical_window_width_ = current_screen_width_;
physical_window_height_ = current_screen_height_;
} else if (fullscreen_enabled_) {
// En fullscreen F3, obtener tamaño REAL del display (no del framebuffer lógico)
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays != nullptr && num_displays > 0) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm != nullptr) {
physical_window_width_ = dm->w;
physical_window_height_ = dm->h;
}
SDL_free(displays);
}
} else {
// En modo ventana, obtener tamaño FÍSICO real del framebuffer
int window_w = 0, window_h = 0;
SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
physical_window_width_ = window_w;
physical_window_height_ = window_h;
}
// Notificar a UIManager del cambio de tamaño (delegado)
// Pasar current_screen_height_ para que UIManager actualice la altura lógica
// (necesario en F4 donde la resolución lógica cambia a la del display)
ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_height_,
current_screen_height_);
}
// ============================================================================
// MÉTODOS PÚBLICOS PARA STATEMANAGER (automatización sin notificación)
// ============================================================================
void Engine::enterShapeMode(ShapeType type) {
activateShapeInternal(type);
}
void Engine::exitShapeMode(bool force_gravity) {
toggleShapeModeInternal(force_gravity);
}
void Engine::switchTextureSilent() {
switchTextureInternal(false);
}
void Engine::setTextureByIndex(size_t index) {
if (index >= textures_.size()) return;
current_texture_index_ = index;
texture_ = textures_[current_texture_index_];
int new_size = texture_->getWidth();
current_ball_size_ = new_size;
scene_manager_->updateBallTexture(texture_, new_size);
}
// Toggle manual del Modo Logo (tecla K)
// Sistema de cambio de sprites dinámico
void Engine::switchTextureInternal(bool show_notification) {
if (textures_.empty()) return;
// Cambiar a siguiente textura (ciclar)
current_texture_index_ = (current_texture_index_ + 1) % textures_.size();
texture_ = textures_[current_texture_index_];
// Obtener nuevo tamaño de la textura
int new_size = texture_->getWidth();
current_ball_size_ = new_size;
// Actualizar texturas y tamaños de todas las pelotas (delegado a SceneManager)
scene_manager_->updateBallTexture(texture_, new_size);
// Mostrar notificación con el nombre de la textura (solo si se solicita)
if (show_notification) {
std::string texture_name = texture_names_[current_texture_index_];
std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::tolower);
showNotificationForAction("Textura " + texture_name);
}
}
// ============================================================================
// Sistema de Figuras 3D - THIN WRAPPERS (delegan a ShapeManager)
// ============================================================================
void Engine::toggleShapeModeInternal(bool force_gravity_on_exit) {
shape_manager_->toggleShapeMode(force_gravity_on_exit);
current_mode_ = shape_manager_->getCurrentMode();
}
void Engine::activateShapeInternal(ShapeType type) {
shape_manager_->activateShape(type);
current_mode_ = SimulationMode::SHAPE;
}
void Engine::generateShape() {
shape_manager_->generateShape();
}
void Engine::updateShape() {
shape_manager_->update(delta_time_);
}
// ============================================================================
// BENCHMARK DE RENDIMIENTO
// ============================================================================
void Engine::runPerformanceBenchmark() {
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
float monitor_hz = 60.0f;
if (displays && num_displays > 0) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm && dm->refresh_rate > 0) monitor_hz = dm->refresh_rate;
SDL_free(displays);
}
// Benchmark CPU-only: sin render(), sin GPU, ventana permanece oculta
const int BENCH_DURATION_MS = 600;
const int WARMUP_ITERS = 10;
SimulationMode original_mode = current_mode_;
auto restore = [&]() {
current_mode_ = original_mode;
if (shape_manager_->isShapeModeActive()) {
shape_manager_->toggleShapeMode(false);
}
scene_manager_->changeScenario(0, original_mode);
last_frame_time_ = 0;
};
custom_auto_available_ = false;
if (custom_scenario_enabled_) {
scene_manager_->changeScenario(CUSTOM_SCENARIO_IDX, SimulationMode::SHAPE);
activateShapeInternal(ShapeType::SPHERE);
last_frame_time_ = 0;
for (int w = 0; w < WARMUP_ITERS; ++w) {
calculateDeltaTime();
update();
}
int frame_count = 0;
Uint64 start = SDL_GetTicks();
while (SDL_GetTicks() - start < static_cast<Uint64>(BENCH_DURATION_MS)) {
calculateDeltaTime();
update();
++frame_count;
}
float ups = static_cast<float>(frame_count) / (BENCH_DURATION_MS / 1000.0f);
custom_auto_available_ = (ups >= monitor_hz);
std::cout << "[Benchmark CPU] Custom (" << custom_scenario_balls_ << " bolas): "
<< ups << " ups → " << (custom_auto_available_ ? "OK" : "insuficiente") << "\n";
}
for (int idx = DEMO_AUTO_MAX_SCENARIO; idx >= DEMO_AUTO_MIN_SCENARIO; --idx) {
scene_manager_->changeScenario(idx, SimulationMode::SHAPE);
activateShapeInternal(ShapeType::SPHERE);
last_frame_time_ = 0;
for (int w = 0; w < WARMUP_ITERS; ++w) {
calculateDeltaTime();
update();
}
int frame_count = 0;
Uint64 start = SDL_GetTicks();
while (SDL_GetTicks() - start < static_cast<Uint64>(BENCH_DURATION_MS)) {
calculateDeltaTime();
update();
++frame_count;
}
float measured_ups = static_cast<float>(frame_count) / (BENCH_DURATION_MS / 1000.0f);
int ball_count = BALL_COUNT_SCENARIOS[idx];
if (measured_ups >= monitor_hz) {
std::cout << "[Benchmark CPU] Escenario " << idx << " (" << ball_count << " bolas): "
<< measured_ups << " ups → OK\n";
std::cout << "[Benchmark CPU] Resultado: max escenario auto = " << idx
<< " (" << ball_count << " bolas)\n";
max_auto_scenario_ = idx;
restore();
return;
}
std::cout << "[Benchmark CPU] Escenario " << idx << " (" << ball_count << " bolas): "
<< measured_ups << " ups → insuficiente\n";
}
std::cout << "[Benchmark CPU] Resultado: max escenario auto = " << DEMO_AUTO_MIN_SCENARIO
<< " (" << BALL_COUNT_SCENARIOS[DEMO_AUTO_MIN_SCENARIO] << " bolas)\n";
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 renderer de software (DESTRUIR renderer PRIMER, després surface)
if (ui_renderer_) { SDL_DestroyRenderer(ui_renderer_); ui_renderer_ = nullptr; }
if (ui_surface_) { SDL_DestroySurface(ui_surface_); ui_surface_ = nullptr; }
ui_surface_ = SDL_CreateSurface(current_screen_width_, current_screen_height_, SDL_PIXELFORMAT_RGBA32);
if (ui_surface_) {
ui_renderer_ = SDL_CreateSoftwareRenderer(ui_surface_);
}
// Re-inicialitzar components UI amb nou renderer
if (ui_renderer_ && ui_manager_) {
ui_manager_->initialize(ui_renderer_, theme_manager_.get(),
current_screen_width_, current_screen_height_, // physical
base_screen_width_, base_screen_height_); // logical (font size based on base)
}
if (ui_renderer_ && app_logo_) {
app_logo_->initialize(ui_renderer_, current_screen_width_, current_screen_height_);
}
}
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);
}