Pulsar B en escenarios 6-8 (o custom >1K) no activa boids y muestra notificación. Cambiar de escenario estando en BOIDS queda bloqueado si el destino supera BOIDS_MAX_BALLS (= SCENE_BALLS_5). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1600 lines
67 KiB
C++
1600 lines
67 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 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 = 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;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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);
|
||
} |