Files
vibe3_physics/source/engine.cpp
Sergio Valor 310c6d244e fix(engine): corregir figura 3D i text en real fullscreen
- Bug 2: moure shape_manager_->updateScreenSize() fora del bloc
  condicional SHAPE a les dues branques de toggleRealFullscreen(),
  de manera que ShapeManager sempre té les dimensions correctes quan
  s'activa una figura després d'entrar en fullscreen
- Bug text: passar base_screen_width_/height_ com a dimensions lògiques
  a ui_manager_->initialize() en recreateOffscreenTexture(), evitant
  que calculateFontSize() s'avaluï amb la resolució nativa de fullscreen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 22:51:31 +01:00

1418 lines
58 KiB
C++

#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 zoom calculado ANTES de crear la ventana (para F1/F2/F3/F4)
current_window_zoom_ = 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 = 0;
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;
}
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)));
// 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();
// 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;
}
}
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_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_.time += delta_time_;
// 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) {
// Modo 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)
void Engine::toggleBoidsMode(bool force_gravity_on) {
if (current_mode_ == SimulationMode::BOIDS) {
// Salir del modo boids (velocidades ya son time-based, no requiere conversión)
current_mode_ = SimulationMode::PHYSICS;
boid_manager_->deactivateBoids(force_gravity_on); // Pasar parámetro para control preciso
} else {
// Entrar al modo boids (desde PHYSICS o SHAPE)
if (current_mode_ == SimulationMode::SHAPE) {
// Si estamos en modo shape, salir primero sin forzar gravedad
shape_manager_->toggleShapeMode(false);
current_mode_ = SimulationMode::PHYSICS;
}
// Activar modo boids
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) {
// 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();
if (current_mode_ == SimulationMode::SHAPE) {
// 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);
}
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,
static_cast<float>(current_screen_width_),
static_cast<float>(current_screen_height_));
}
depth_buckets_[b].clear();
}
} else {
size_t idx = 0;
for (const auto& ball : balls) {
SDL_FRect pos = ball->getPosition();
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++;
}
}
// 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;
}
GpuTexture* sprite_tex = (!gpu_textures_.empty())
? gpu_textures_[current_texture_index_].get() : nullptr;
// === 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;
SDL_GPURenderPass* pass1 = SDL_BeginGPURenderPass(cmd, &ct, 1, nullptr);
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);
// 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);
}
// 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);
}
SDL_EndGPURenderPass(pass1);
}
// === 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_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);
}
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_) {
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
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_;
// Restaurar ventana normal con el zoom actual (no hardcoded)
SDL_SetWindowFullscreen(window_, false);
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
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::handlePostFXCycle() {
static constexpr float presets[5][3] = {
{1.5f, 0.0f, 0.0f}, {1.5f, 0.0f, 0.8f},
{1.5f, 1.0f, 0.0f}, {1.5f, 1.0f, 0.8f},
{0.0f, 0.0f, 0.0f}
};
static constexpr const char* names[5] = {
"PostFX: Vinyeta", "PostFX: Scanlines",
"PostFX: Cromàtica", "PostFX: Complet", "PostFX: Desactivat"
};
postfx_effect_mode_ = (postfx_effect_mode_ + 1) % 5;
postfx_uniforms_.vignette_strength = presets[postfx_effect_mode_][0];
postfx_uniforms_.chroma_strength = presets[postfx_effect_mode_][1];
postfx_uniforms_.scanline_strength = presets[postfx_effect_mode_][2];
showNotificationForAction(names[postfx_effect_mode_]);
}
void Engine::toggleIntegerScaling() {
// Solo permitir cambio si estamos en modo fullscreen normal (F3)
if (!fullscreen_enabled_) {
return; // No hacer nada si no estamos en fullscreen
}
// 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;
}
// SDL_GPU stretches to fill swapchain by default; just show notification
const char* mode_name = "INTEGER";
switch (current_scaling_mode_) {
case ScalingMode::INTEGER: mode_name = "INTEGER"; break;
case ScalingMode::LETTERBOX: mode_name = "LETTERBOX"; break;
case ScalingMode::STRETCH: mode_name = "STRETCH"; break;
}
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) {
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
int Engine::calculateMaxWindowZoom() const {
// Obtener información del display usando el método de Coffee Crisis
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays == nullptr || num_displays == 0) {
return WINDOW_ZOOM_MIN; // Fallback si no se puede obtener
}
// Obtener el modo de display actual
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm == nullptr) {
SDL_free(displays);
return WINDOW_ZOOM_MIN;
}
// Calcular zoom máximo usando la fórmula de Coffee Crisis
const int MAX_ZOOM = std::min(dm->w / base_screen_width_, (dm->h - WINDOW_DECORATION_HEIGHT) / base_screen_height_);
SDL_free(displays);
// Aplicar límites
return std::max(WINDOW_ZOOM_MIN, std::min(MAX_ZOOM, WINDOW_ZOOM_MAX));
}
void Engine::setWindowZoom(int new_zoom) {
// Validar zoom
int max_zoom = calculateMaxWindowZoom();
new_zoom = std::max(WINDOW_ZOOM_MIN, std::min(new_zoom, max_zoom));
if (new_zoom == current_window_zoom_) {
return; // No hay cambio
}
// Obtener posición actual del centro de la ventana
int current_x, current_y;
SDL_GetWindowPosition(window_, &current_x, &current_y);
int current_center_x = current_x + (base_screen_width_ * current_window_zoom_) / 2;
int current_center_y = current_y + (base_screen_height_ * current_window_zoom_) / 2;
// Calcular nuevo tamaño
int new_width = base_screen_width_ * new_zoom;
int new_height = base_screen_height_ * new_zoom;
// Calcular nueva posición (centrada en el punto actual)
int new_x = current_center_x - new_width / 2;
int new_y = current_center_y - new_height / 2;
// Obtener límites del escritorio para no salirse
SDL_Rect display_bounds;
if (SDL_GetDisplayBounds(SDL_GetPrimaryDisplay(), &display_bounds) == 0) {
// Aplicar márgenes
int min_x = WINDOW_DESKTOP_MARGIN;
int min_y = WINDOW_DESKTOP_MARGIN;
int max_x = display_bounds.w - new_width - WINDOW_DESKTOP_MARGIN;
int max_y = display_bounds.h - new_height - WINDOW_DESKTOP_MARGIN - WINDOW_DECORATION_HEIGHT;
// Limitar posición
new_x = std::max(min_x, std::min(new_x, max_x));
new_y = std::max(min_y, std::min(new_y, max_y));
}
// Aplicar cambios
SDL_SetWindowSize(window_, new_width, new_height);
SDL_SetWindowPosition(window_, new_x, new_y);
current_window_zoom_ = new_zoom;
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
}
void Engine::zoomIn() {
setWindowZoom(current_window_zoom_ + 1);
}
void Engine::zoomOut() {
setWindowZoom(current_window_zoom_ - 1);
}
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)
ui_manager_->updatePhysicalWindowSize(physical_window_width_, physical_window_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(), ::toupper);
showNotificationForAction("Sprite: " + 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);
}
SDL_HideWindow(window_);
if (gpu_ctx_) gpu_ctx_->setVSync(false); // Disable VSync for benchmark
const int BENCH_DURATION_MS = 600;
const int WARMUP_FRAMES = 5;
SimulationMode original_mode = current_mode_;
auto restore = [&]() {
if (gpu_ctx_) gpu_ctx_->setVSync(vsync_enabled_);
SDL_ShowWindow(window_);
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_FRAMES; ++w) {
calculateDeltaTime();
SDL_Event e; while (SDL_PollEvent(&e)) {}
update();
render();
}
int frame_count = 0;
Uint64 start = SDL_GetTicks();
while (SDL_GetTicks() - start < static_cast<Uint64>(BENCH_DURATION_MS)) {
calculateDeltaTime();
SDL_Event e; while (SDL_PollEvent(&e)) {}
update();
render();
++frame_count;
}
float fps = static_cast<float>(frame_count) / (BENCH_DURATION_MS / 1000.0f);
custom_auto_available_ = (fps >= monitor_hz);
}
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_FRAMES; ++w) {
calculateDeltaTime();
SDL_Event e; while (SDL_PollEvent(&e)) {}
update();
render();
}
int frame_count = 0;
Uint64 start = SDL_GetTicks();
while (SDL_GetTicks() - start < static_cast<Uint64>(BENCH_DURATION_MS)) {
calculateDeltaTime();
SDL_Event e;
while (SDL_PollEvent(&e)) {}
update();
render();
++frame_count;
}
float measured_fps = static_cast<float>(frame_count) / (BENCH_DURATION_MS / 1000.0f);
if (measured_fps >= monitor_hz) {
max_auto_scenario_ = idx;
restore();
return;
}
}
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);
}