Files
coffee_crisis_arcade_edition/source/screen.cpp

538 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "screen.hpp"
#include <SDL3/SDL.h> // Para SDL_SetRenderTarget, SDL_RenderTexture, SDL_SetRenderDrawColor, SDL_SetRenderVSync, SDL_LogCategory, SDL_GetError, SDL_LogError, SDL_LogInfo, SDL_RendererLogicalPresentation, SDL_SetRenderLogicalPresentation, SDL_CreateTexture, SDL_DestroyTexture, SDL_DestroyWindow, SDL_GetDisplayName, SDL_GetTicks, SDL_Quit, SDL_RENDERER_VSYNC_DISABLED, SDL_RenderClear, SDL_CreateRenderer, SDL_CreateWindow, SDL_DestroyRenderer, SDL_DisplayID, SDL_FRect, SDL_GetCurrentDisplayMode, SDL_GetDisplays, SDL_GetRenderTarget, SDL_GetWindowPosition, SDL_GetWindowSize, SDL_Init, SDL_LogWarn, SDL_PixelFormat, SDL_RenderFillRect, SDL_RenderPresent, SDL_SetHint, SDL_SetRenderDrawBlendMode, SDL_SetTextureScaleMode, SDL_SetWindowFullscreen, SDL_SetWindowPosition, SDL_SetWindowSize, SDL_TextureAccess, SDL_free, SDL_BLENDMODE_BLEND, SDL_HINT_RENDER_DRIVER, SDL_INIT_VIDEO, SDL_ScaleMode, SDL_WINDOW_FULLSCREEN, SDL_WindowFlags
#include <algorithm> // Para min, max
#include <cstring> // Para memcpy
#include <memory> // Para allocator, shared_ptr, unique_ptr, __shared_ptr_access, make_shared, make_unique
#include <string> // Para basic_string, operator+, char_traits, to_string, string
#include <vector> // Para vector
#include "asset.hpp" // Para Asset
#include "mouse.hpp" // Para updateCursorVisibility
#include "options.hpp" // Para Video, video, Window, window
#include "param.hpp" // Para Param, param, ParamGame, ParamDebug
#include "rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#include "text.hpp" // Para Text
#include "texture.hpp" // Para Texture
#include "ui/logger.hpp" // Para info
#include "ui/notifier.hpp" // Para Notifier
#include "ui/service_menu.hpp" // Para ServiceMenu
// Singleton
Screen* Screen::instance = nullptr;
// Inicializa la instancia única del singleton
void Screen::init() {
Screen::instance = new Screen();
Screen::initPostFX(); // Llamar aquí para que Screen::get() ya devuelva la instancia
}
// Libera la instancia
void Screen::destroy() { delete Screen::instance; }
// Obtiene la instancia
auto Screen::get() -> Screen* { return Screen::instance; }
// Constructor
Screen::Screen()
: window_(nullptr),
renderer_(nullptr),
game_canvas_(nullptr),
service_menu_(nullptr),
notifier_(nullptr),
src_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}),
dst_rect_(SDL_FRect{.x = 0, .y = 0, .w = param.game.width, .h = param.game.height}) {
// Arranca SDL VIDEO, crea la ventana y el renderizador
initSDLVideo();
// Crea la textura de destino
game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
SDL_SetTextureScaleMode(game_canvas_, SDL_SCALEMODE_NEAREST);
// Inicializar buffer de píxeles para SDL3GPU
pixel_buffer_.resize(static_cast<size_t>(param.game.width) * static_cast<size_t>(param.game.height));
// Crea el objeto de texto
createText();
#ifdef _DEBUG
debug_info_.text = text_;
setDebugInfoEnabled(true);
#endif
// Renderizar una vez la textura vacía para que tenga contenido válido antes de inicializar los shaders (evita pantalla negra)
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
// Limpiar renderer
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255);
SDL_RenderClear(renderer_);
SDL_RenderPresent(renderer_);
}
// Destructor
Screen::~Screen() {
SDL_DestroyTexture(game_canvas_);
SDL_DestroyRenderer(renderer_);
SDL_DestroyWindow(window_);
}
// Limpia la pantalla
void Screen::clean(Color color) {
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
SDL_RenderClear(renderer_);
}
// Prepara para empezar a dibujar en la textura de juego
void Screen::start() { SDL_SetRenderTarget(renderer_, game_canvas_); }
// Vuelca el contenido del renderizador en pantalla
void Screen::render() {
fps_.increment();
renderOverlays(); // Renderiza todos los overlays y efectos
renderPresent(); // Renderiza el contenido del game_canvas_
}
// Vuelca el contenido del renderizador en pantalla exceptuando ciertas partes
void Screen::coreRender() {
/*fps_.increment();
#ifdef _DEBUG
renderInfo();
#endif*/
renderPresent(); // Renderiza el contenido del game_canvas_
}
// Renderiza el contenido del game_canvas_
void Screen::renderPresent() {
#ifndef NO_SHADERS
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
// Leer píxeles de game_canvas_ con la API SDL3 (devuelve SDL_Surface*)
SDL_SetRenderTarget(renderer_, game_canvas_);
SDL_Surface* surface = SDL_RenderReadPixels(renderer_, nullptr);
if (surface != nullptr) {
if (surface->format == SDL_PIXELFORMAT_ARGB8888) {
std::memcpy(pixel_buffer_.data(), surface->pixels,
pixel_buffer_.size() * sizeof(Uint32));
} else {
SDL_Surface* converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
if (converted != nullptr) {
std::memcpy(pixel_buffer_.data(), converted->pixels,
pixel_buffer_.size() * sizeof(Uint32));
SDL_DestroySurface(converted);
}
}
SDL_DestroySurface(surface);
}
SDL_SetRenderTarget(renderer_, nullptr);
// Subir a GPU y presentar con PostFX
shader_backend_->uploadPixels(pixel_buffer_.data(), param.game.width, param.game.height);
shader_backend_->render();
return;
}
#endif
// Fallback: SDL_Renderer
SDL_SetRenderTarget(renderer_, nullptr);
clean();
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
}
// Establece el modo de video
void Screen::setFullscreenMode() {
SDL_SetWindowFullscreen(window_, Options::video.fullscreen);
}
// Camibia entre pantalla completa y ventana
void Screen::toggleFullscreen() {
Options::video.fullscreen = !Options::video.fullscreen;
setFullscreenMode();
}
// Cambia el tamaño de la ventana
void Screen::setWindowZoom(int zoom) {
Options::window.zoom = zoom;
adjustWindowSize();
}
// Reduce el tamaño de la ventana
auto Screen::decWindowSize() -> bool {
if (!Options::video.fullscreen) {
const int PREVIOUS_ZOOM = Options::window.zoom;
--Options::window.zoom;
Options::window.zoom = std::max(Options::window.zoom, 1);
if (Options::window.zoom != PREVIOUS_ZOOM) {
adjustWindowSize();
return true;
}
}
return false;
}
// Aumenta el tamaño de la ventana
auto Screen::incWindowSize() -> bool {
if (!Options::video.fullscreen) {
const int PREVIOUS_ZOOM = Options::window.zoom;
++Options::window.zoom;
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
if (Options::window.zoom != PREVIOUS_ZOOM) {
adjustWindowSize();
return true;
}
}
return false;
}
// Recibe deltaTime de las secciones y actualiza la lógica
void Screen::update(float delta_time) {
fps_.calculate(SDL_GetTicks());
shake_effect_.update(src_rect_, dst_rect_, delta_time);
flash_effect_.update(delta_time);
if (service_menu_ != nullptr) {
service_menu_->update(delta_time);
}
if (notifier_ != nullptr) {
notifier_->update(delta_time);
}
Mouse::updateCursorVisibility();
}
// Actualiza los elementos mínimos
void Screen::coreUpdate() {
fps_.calculate(SDL_GetTicks());
Mouse::updateCursorVisibility();
}
// Actualiza y dibuja el efecto de flash en la pantalla
void Screen::renderFlash() {
if (flash_effect_.isRendarable()) {
SDL_SetRenderDrawColor(renderer_, flash_effect_.color.r, flash_effect_.color.g, flash_effect_.color.b, 0xFF);
SDL_RenderClear(renderer_);
}
}
// Aplica el efecto de agitar la pantalla
void Screen::renderShake() {
if (shake_effect_.enabled) {
// Guarda el renderizador actual para dejarlo despues como estaba
auto* current_target = SDL_GetRenderTarget(renderer_);
// Crea una textura temporal
auto* temp_texture = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, param.game.width, param.game.height);
// Vuelca game_canvas_ a la textura temporal
SDL_SetRenderTarget(renderer_, temp_texture);
SDL_RenderTexture(renderer_, game_canvas_, nullptr, nullptr);
// Vuelca textura temporal a game_canvas_
SDL_SetRenderTarget(renderer_, game_canvas_);
SDL_RenderTexture(renderer_, temp_texture, &src_rect_, &dst_rect_);
// Elimina la textura temporal
SDL_DestroyTexture(temp_texture);
// Restaura el renderizador de destino original
SDL_SetRenderTarget(renderer_, current_target);
}
}
#ifdef _DEBUG
// Muestra información por pantalla
void Screen::renderInfo() const {
if (debug_info_.show) {
// Resolution
debug_info_.text->writeDX(Text::COLOR | Text::STROKE, param.game.width - debug_info_.text->length(Options::video.info) - 2, 1, Options::video.info, 1, param.debug.color, 1, param.debug.color.DARKEN(150));
// FPS
const std::string FPS_TEXT = std::to_string(fps_.last_value) + " FPS";
debug_info_.text->writeDX(Text::COLOR | Text::STROKE, param.game.width - debug_info_.text->length(FPS_TEXT) - 2, 1 + debug_info_.text->getCharacterSize(), FPS_TEXT, 1, param.debug.color, 1, param.debug.color.DARKEN(150));
#ifdef RECORDING
// RECORDING
debug_info_.text->writeDX(Text::COLOR | Text::STROKE, param.game.width - debug_info_.text->length("RECORDING"), 2 * (1 + debug_info_.text->getCharacterSize()), "RECORDING", 1, param.debug.color, 1, param.debug.color.DARKEN(150));
#endif
}
}
#endif
// Inicializa PostFX (SDL3GPU)
void Screen::initPostFX() {
#ifndef NO_SHADERS
auto* self = Screen::get();
if (self == nullptr) {
SDL_Log("Screen::initPostFX: instance is null, skipping");
return;
}
if (!self->shader_backend_) {
self->shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
}
if (!self->shader_backend_->isHardwareAccelerated()) {
const bool ok = self->shader_backend_->init(self->window_, self->game_canvas_, "", "");
SDL_Log("Screen::initPostFX: SDL3GPUShader::init() = %s", ok ? "OK" : "FAILED");
}
SDL_Log("Screen::initPostFX: presets=%d current=%d postfx=%s",
static_cast<int>(Options::postfx_presets.size()),
Options::current_postfx_preset,
Options::video.postfx ? "ON" : "OFF");
self->applyCurrentPostFXPreset();
#endif
}
// Calcula el tamaño de la ventana
void Screen::adjustWindowSize() {
if (!Options::video.fullscreen) {
// Establece el nuevo tamaño
const int WIDTH = param.game.width * Options::window.zoom;
const int HEIGHT = param.game.height * Options::window.zoom;
int old_width;
int old_height;
SDL_GetWindowSize(window_, &old_width, &old_height);
int old_pos_x;
int old_pos_y;
SDL_GetWindowPosition(window_, &old_pos_x, &old_pos_y);
const int NEW_POS_X = old_pos_x + ((old_width - WIDTH) / 2);
const int NEW_POS_Y = old_pos_y + ((old_height - HEIGHT) / 2);
SDL_SetWindowPosition(window_, std::max(NEW_POS_X, WINDOWS_DECORATIONS), std::max(NEW_POS_Y, 0));
SDL_SetWindowSize(window_, WIDTH, HEIGHT);
}
}
// Renderiza todos los overlays y efectos
void Screen::renderOverlays() {
// Dibuja efectos y elementos sobre el game_canvas_
renderShake();
renderFlash();
renderAttenuate();
service_menu_->render();
notifier_->render();
#ifdef _DEBUG
renderInfo();
#endif
}
// Atenua la pantalla
void Screen::renderAttenuate() {
if (attenuate_effect_) {
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 64);
SDL_RenderFillRect(renderer_, nullptr);
}
}
// Arranca SDL VIDEO y crea la ventana
auto Screen::initSDLVideo() -> bool {
// Inicializar SDL
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"FATAL: Failed to initialize SDL_VIDEO! SDL Error: %s",
SDL_GetError());
return false;
}
// Obtener información de la pantalla
getDisplayInfo();
// Configurar hint para renderizado
#ifdef __APPLE__
if (!SDL_SetHint(SDL_HINT_RENDER_DRIVER, "metal")) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Failed to set Metal hint!");
}
#endif
// Crear ventana
#ifdef __APPLE__
SDL_WindowFlags window_flags = SDL_WINDOW_METAL;
#else
SDL_WindowFlags window_flags = 0;
#endif
if (Options::video.fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN;
}
window_flags |= SDL_WINDOW_HIDDEN;
window_ = SDL_CreateWindow(
Options::window.caption.c_str(),
param.game.width * Options::window.zoom,
param.game.height * Options::window.zoom,
window_flags);
if (window_ == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"FATAL: Failed to create window! SDL Error: %s",
SDL_GetError());
SDL_Quit();
return false;
}
// Crear renderer
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"FATAL: Failed to create renderer! SDL Error: %s",
SDL_GetError());
SDL_DestroyWindow(window_);
window_ = nullptr;
SDL_Quit();
return false;
}
// Configurar renderer
SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
Logger::info("Video system initialized successfully");
return true;
}
// Obtiene información sobre la pantalla
void Screen::getDisplayInfo() {
int i;
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays != nullptr) {
for (i = 0; i < num_displays; ++i) {
SDL_DisplayID instance_id = displays[i];
const char* name = SDL_GetDisplayName(instance_id);
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Display %" SDL_PRIu32 ": %s", instance_id, (name != nullptr) ? name : "Unknown");
Logger::info(std::string("Display ") + std::to_string(instance_id) + ": " + (name != nullptr ? name : "Unknown"));
}
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
// Guarda información del monitor en display_monitor_
const char* first_display_name = SDL_GetDisplayName(displays[0]);
display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
display_monitor_.width = static_cast<int>(dm->w);
display_monitor_.height = static_cast<int>(dm->h);
display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
Options::window.max_zoom = std::min(dm->w / param.game.width, dm->h / param.game.height);
Options::window.zoom = std::min(Options::window.zoom, Options::window.max_zoom);
// Obtiene la cadena con la información sobre la resolución y el refresco
Options::video.info = std::to_string(dm->w) + "x" +
std::to_string(dm->h) + " @ " +
std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
// Muestra información sobre el tamaño de la pantalla y de la ventana de juego
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Current display mode: %dx%d @ %dHz", static_cast<int>(dm->w), static_cast<int>(dm->h), static_cast<int>(dm->refresh_rate));
Logger::info("Current display mode: " + Options::video.info);
// SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Window resolution: %dx%d x%d", static_cast<int>(param.game.width), static_cast<int>(param.game.height), Options::window.zoom);
Logger::info("Window resolution: " + std::to_string(static_cast<int>(param.game.width)) + "x" + std::to_string(static_cast<int>(param.game.height)) + "x" + std::to_string(Options::window.zoom));
// Calcula el máximo factor de zoom que se puede aplicar a la pantalla
const int MAX_ZOOM = std::min(dm->w / param.game.width, (dm->h - WINDOWS_DECORATIONS) / param.game.height);
// Normaliza los valores de zoom
Options::window.zoom = std::min(Options::window.zoom, MAX_ZOOM);
SDL_free(displays);
}
}
// Alterna entre activar y desactivar los efectos PostFX
void Screen::togglePostFX() {
Options::video.postfx = !Options::video.postfx;
auto* self = Screen::get();
if (self != nullptr) {
self->applyCurrentPostFXPreset();
}
}
// Avanza al siguiente preset PostFX
void Screen::nextPostFXPreset() {
if (Options::postfx_presets.empty()) { return; }
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
auto* self = Screen::get();
if (self != nullptr) {
self->applyCurrentPostFXPreset();
}
}
// Alterna entre activar y desactivar el supersampling 3x
void Screen::toggleSupersampling() {
Options::video.supersampling = (Options::video.supersampling % 3) + 1;
auto* self = Screen::get();
if (self != nullptr && self->shader_backend_ && self->shader_backend_->isHardwareAccelerated()) {
self->applyCurrentPostFXPreset();
}
}
// Aplica el preset PostFX activo al backend
void Screen::applyCurrentPostFXPreset() {
if (!shader_backend_) { return; }
// setOversample PRIMERO: puede recrear texturas antes de que setPostFXParams
// decida si hornear scanlines en CPU o aplicarlas en GPU.
shader_backend_->setOversample(Options::video.supersampling);
Rendering::PostFXParams p{};
if (Options::video.postfx && !Options::postfx_presets.empty()) {
const auto& preset = Options::postfx_presets.at(static_cast<size_t>(Options::current_postfx_preset));
p.vignette = preset.vignette;
p.scanlines = preset.scanlines;
p.chroma = preset.chroma;
p.mask = preset.mask;
p.gamma = preset.gamma;
p.curvature = preset.curvature;
p.bleeding = preset.bleeding;
p.flicker = preset.flicker;
SDL_Log("Screen::applyCurrentPostFXPreset: preset='%s' scan=%.2f vign=%.2f chroma=%.2f ss=%d×",
preset.name.c_str(), p.scanlines, p.vignette, p.chroma, Options::video.supersampling);
} else {
SDL_Log("Screen::applyCurrentPostFXPreset: PostFX=%s presets=%d → passthrough",
Options::video.postfx ? "ON" : "OFF",
static_cast<int>(Options::postfx_presets.size()));
}
shader_backend_->setPostFXParams(p);
}
// Alterna entre activar y desactivar el escalado entero
void Screen::toggleIntegerScale() {
Options::video.integer_scale = !Options::video.integer_scale;
SDL_SetRenderLogicalPresentation(renderer_, param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
if (shader_backend_) {
shader_backend_->setScaleMode(Options::video.integer_scale);
}
}
// Alterna entre activar y desactivar el V-Sync
void Screen::toggleVSync() {
Options::video.vsync = !Options::video.vsync;
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
if (shader_backend_) {
shader_backend_->setVSync(Options::video.vsync);
}
}
// Establece el estado del V-Sync
void Screen::setVSync(bool enabled) {
Options::video.vsync = enabled;
SDL_SetRenderVSync(renderer_, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
}
// Obtiene los punteros a los singletones
void Screen::getSingletons() {
service_menu_ = ServiceMenu::get();
notifier_ = Notifier::get();
}
// Aplica los valores de las opciones
void Screen::applySettings() {
SDL_SetRenderVSync(renderer_, Options::video.vsync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
SDL_SetRenderLogicalPresentation(Screen::get()->getRenderer(), param.game.width, param.game.height, Options::video.integer_scale ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX);
setFullscreenMode();
adjustWindowSize();
}
// Crea el objeto de texto
void Screen::createText() {
auto texture = std::make_shared<Texture>(getRenderer(), Asset::get()->getPath("aseprite.png"));
text_ = std::make_shared<Text>(texture, Asset::get()->getPath("aseprite.txt"));
}