#include "core/rendering/screen.hpp" #include #include #include "core/locale/locale.hpp" #include "core/rendering/overlay.hpp" #ifndef NO_SHADERS #include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" #endif #include "game/defines.hpp" #include "game/options.hpp" #include "utils/utils.hpp" Screen* Screen::instance_ = nullptr; void Screen::init() { instance_ = new Screen(); } void Screen::destroy() { delete instance_; instance_ = nullptr; } auto Screen::get() -> Screen* { return instance_; } Screen::Screen() { // Carrega opcions guardades zoom_ = Options::window.zoom; fullscreen_ = Options::window.fullscreen; calculateMaxZoom(); if (zoom_ < 1) zoom_ = 1; if (zoom_ > max_zoom_) zoom_ = max_zoom_; // Clamp de la resolució interna a [1, max_zoom_]. Llegir del YAML i // ajustar aquí és l'únic moment en què es fa — el menú re-clampa cada // canvi. Si la pantalla és més petita que el valor desat (p.ex. canvi // de monitor), baixem al màxim suportat. if (Options::video.internal_resolution < 1) Options::video.internal_resolution = 1; if (Options::video.internal_resolution > max_zoom_) Options::video.internal_resolution = max_zoom_; int w = GAME_WIDTH * zoom_; int h = Options::video.aspect_ratio_4_3 ? static_cast(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_; window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0); renderer_ = SDL_CreateRenderer(window_, nullptr); texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT); applyFallbackPresentation(); // Inicialitza backend GPU si l'acceleració està activada initShaders(); std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n"; } Screen::~Screen() { // Guarda opcions abans de destruir Options::window.zoom = zoom_; Options::window.fullscreen = fullscreen_; // Destrueix el backend GPU (només existeix si s'ha compilat amb shaders) if (shader_backend_) { #ifndef NO_SHADERS auto* gpu = dynamic_cast(shader_backend_.get()); if (gpu) gpu->destroy(); #endif shader_backend_.reset(); } if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_); if (texture_) SDL_DestroyTexture(texture_); if (renderer_) SDL_DestroyRenderer(renderer_); if (window_) SDL_DestroyWindow(window_); } void Screen::initShaders() { #ifdef NO_SHADERS // Build sense shaders (p.ex. emscripten/WebGL2, on SDL3 GPU no està // disponible). Es salta tota la inicialització — shader_backend_ es // queda nul·lptr i tots els `if (shader_backend_)` del render path // curtcircuiten cap al fallback SDL_Renderer. return; #else if (!Options::video.gpu_acceleration) return; shader_backend_ = std::make_unique(); const std::string FALLBACK_DRIVER = "none"; shader_backend_->setPreferredDriver( Options::video.gpu_acceleration ? "" : FALLBACK_DRIVER); // init() rep la finestra i la textura (la textura s'usa com a referència, el GPU fa uploadPixels) if (!shader_backend_->init(window_, texture_, "", "")) { std::cerr << "GPU shader backend initialization failed, using SDL_Renderer fallback\n"; shader_backend_.reset(); return; } gpu_driver_ = shader_backend_->getDriverName(); std::cout << "GPU driver: " << gpu_driver_ << '\n'; // Aplica opcions de vídeo shader_backend_->setScalingMode(Options::video.scaling_mode); shader_backend_->setVSync(Options::video.vsync); shader_backend_->setTextureFilter(Options::video.texture_filter); shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3); shader_backend_->setDownscaleAlgo(Options::video.downscale_algo); if (Options::video.supersampling) { shader_backend_->setOversample(3); } shader_backend_->setInternalResolution(Options::video.internal_resolution); // Resol el shader actiu des del config if (Options::video.current_shader == "crtpi") { shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI); } else { shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX); } // Resol presets per nom for (int i = 0; i < static_cast(Options::postfx_presets.size()); i++) { if (Options::postfx_presets[i].name == Options::video.current_postfx_preset) { Options::current_postfx_preset = i; break; } } for (int i = 0; i < static_cast(Options::crtpi_presets.size()); i++) { if (Options::crtpi_presets[i].name == Options::video.current_crtpi_preset) { Options::current_crtpi_preset = i; break; } } applyCurrentPostFXPreset(); applyCurrentCrtPiPreset(); #endif } void Screen::present(Uint32* pixel_data) { fps_.increment(); fps_.calculate(SDL_GetTicks()); updateRenderInfo(); Overlay::render(pixel_data); if (shader_backend_ && shader_backend_->isHardwareAccelerated() && Options::video.shader_enabled) { // Path GPU: puja els píxels i renderitza amb shaders shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT); shader_backend_->render(); } else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { // GPU activa però shaders desactivats: renderitza net (sense efectes). // Força POSTFX amb params zerats — altrament, si l'actiu és CRTPI, // els seus efectes (scanlines, curvatura) seguirien aplicant-se encara // que shader_enabled sigui false. Restaurem l'actiu al final per a // no trencar la selecció de l'usuari. Rendering::PostFXParams clean{}; shader_backend_->setPostFXParams(clean); const auto prev_shader = shader_backend_->getActiveShader(); if (prev_shader != Rendering::ShaderType::POSTFX) { shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX); } shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT); shader_backend_->render(); if (prev_shader != Rendering::ShaderType::POSTFX) { shader_backend_->setActiveShader(prev_shader); } } else { // Fallback SDL_Renderer SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32)); const int mult = Options::video.internal_resolution; if (mult > 1) { // Resolució interna: còpia NN de texture_ → internal_texture_sdl_ // (la fa SDL/GPU, no CPU). Tota la presentació downstream llegirà // d'aquesta textura més gran — el filtre LINEAR final parteix d'una // font més fina i l'estirament a finestra queda menys difús. ensureFallbackInternalTexture(); if (internal_texture_sdl_ != nullptr) { SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST); SDL_SetRenderTarget(renderer_, internal_texture_sdl_); SDL_RenderClear(renderer_); SDL_RenderTexture(renderer_, texture_, nullptr, nullptr); SDL_SetRenderTarget(renderer_, nullptr); // El filtre global (LINEAR/NEAREST) s'aplica a l'estirament final // cap a la finestra; per això l'aplicam a la textura intermèdia. SDL_ScaleMode final_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR) ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST; SDL_SetTextureScaleMode(internal_texture_sdl_, final_scale); SDL_RenderClear(renderer_); SDL_RenderTexture(renderer_, internal_texture_sdl_, nullptr, nullptr); SDL_RenderPresent(renderer_); return; } // Si la creació de la textura ha fallat, caiem al path normal. } SDL_RenderClear(renderer_); SDL_RenderTexture(renderer_, texture_, nullptr, nullptr); SDL_RenderPresent(renderer_); } } void Screen::toggleFullscreen() { fullscreen_ = !fullscreen_; SDL_SetWindowFullscreen(window_, fullscreen_); if (!fullscreen_) { adjustWindowSize(); } } void Screen::incZoom() { if (fullscreen_ || zoom_ >= max_zoom_) return; zoom_++; adjustWindowSize(); } void Screen::decZoom() { if (fullscreen_ || zoom_ <= 1) return; zoom_--; adjustWindowSize(); } void Screen::setZoom(int zoom) { if (zoom < 1 || zoom > max_zoom_ || fullscreen_) return; zoom_ = zoom; adjustWindowSize(); } void Screen::toggleShaders() { Options::video.shader_enabled = !Options::video.shader_enabled; if (Options::video.shader_enabled) { applyCurrentPostFXPreset(); } } auto Screen::toggleSupersampling() -> bool { // SS només té sentit amb shaders on i pipeline PostFX (el Lanczos downscale // i el camí SS s'apliquen al pas de PostFX; CRTPI fa el seu propi // submostreig intern i no usa aquesta via). if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false; if (!Options::video.shader_enabled) return false; if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) return false; Options::video.supersampling = !Options::video.supersampling; shader_backend_->setOversample(Options::video.supersampling ? 3 : 1); return true; } void Screen::toggleAspectRatio() { Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3; if (shader_backend_) { shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3); } else { applyFallbackPresentation(); } if (!fullscreen_) { adjustWindowSize(); } } void Screen::cycleScalingMode(int dir) { constexpr int N = 5; // DISABLED, STRETCH, LETTERBOX, OVERSCAN, INTEGER int cur = static_cast(Options::video.scaling_mode); int step = (dir >= 0) ? 1 : -1; cur = ((cur + step) % N + N) % N; Options::video.scaling_mode = static_cast(cur); if (shader_backend_) { shader_backend_->setScalingMode(Options::video.scaling_mode); } else { applyFallbackPresentation(); } } void Screen::toggleVSync() { Options::video.vsync = !Options::video.vsync; if (shader_backend_) { shader_backend_->setVSync(Options::video.vsync); } } void Screen::cycleTextureFilter(int dir) { // NEAREST <-> LINEAR (només 2 valors, dir no importa més enllà de canviar) (void)dir; Options::video.texture_filter = (Options::video.texture_filter == Options::TextureFilter::LINEAR) ? Options::TextureFilter::NEAREST : Options::TextureFilter::LINEAR; if (shader_backend_) { shader_backend_->setTextureFilter(Options::video.texture_filter); } else { applyFallbackPresentation(); } } void Screen::changeInternalResolution(int dir) { int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1); if (next < 1) next = 1; if (next > max_zoom_) next = max_zoom_; if (next == Options::video.internal_resolution) return; Options::video.internal_resolution = next; // Propaga al backend actiu. Al fallback path, la textura es recrea al // pròxim present via ensureFallbackInternalTexture. if (shader_backend_) { shader_backend_->setInternalResolution(next); } else { applyFallbackPresentation(); } } auto Screen::nextShaderType() -> bool { if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false; if (!Options::video.shader_enabled) return false; if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI); Options::video.current_shader = "crtpi"; applyCurrentCrtPiPreset(); } else { shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX); Options::video.current_shader = "postfx"; applyCurrentPostFXPreset(); } return true; } auto Screen::nextPreset() -> bool { if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false; if (!Options::video.shader_enabled) return false; if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (Options::postfx_presets.empty()) return false; Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast(Options::postfx_presets.size()); Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name; applyCurrentPostFXPreset(); } else { if (Options::crtpi_presets.empty()) return false; Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast(Options::crtpi_presets.size()); Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name; applyCurrentCrtPiPreset(); } return true; } auto Screen::prevShaderType() -> bool { // Només dues opcions — prev == next return nextShaderType(); } auto Screen::prevPreset() -> bool { if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false; if (!Options::video.shader_enabled) return false; if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (Options::postfx_presets.empty()) return false; int n = static_cast(Options::postfx_presets.size()); Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n; Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name; applyCurrentPostFXPreset(); } else { if (Options::crtpi_presets.empty()) return false; int n = static_cast(Options::crtpi_presets.size()); Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n; Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name; applyCurrentCrtPiPreset(); } return true; } auto Screen::getCurrentPresetName() const -> const char* { if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return "---"; if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (Options::current_postfx_preset < static_cast(Options::postfx_presets.size())) return Options::postfx_presets[Options::current_postfx_preset].name.c_str(); } else { if (Options::current_crtpi_preset < static_cast(Options::crtpi_presets.size())) return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str(); } return "---"; } void Screen::setActiveShader(Rendering::ShaderType type) { if (shader_backend_) { shader_backend_->setActiveShader(type); } } void Screen::applyCurrentPostFXPreset() { if (!shader_backend_ || Options::postfx_presets.empty()) return; const auto& preset = Options::postfx_presets[Options::current_postfx_preset]; Rendering::PostFXParams p; 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; shader_backend_->setPostFXParams(p); } void Screen::applyCurrentCrtPiPreset() { if (!shader_backend_ || Options::crtpi_presets.empty()) return; const auto& preset = Options::crtpi_presets[Options::current_crtpi_preset]; Rendering::CrtPiParams p; p.scanline_weight = preset.scanline_weight; p.scanline_gap_brightness = preset.scanline_gap_brightness; p.bloom_factor = preset.bloom_factor; p.input_gamma = preset.input_gamma; p.output_gamma = preset.output_gamma; p.mask_brightness = preset.mask_brightness; p.curvature_x = preset.curvature_x; p.curvature_y = preset.curvature_y; p.mask_type = preset.mask_type; p.enable_scanlines = preset.enable_scanlines; p.enable_multisample = preset.enable_multisample; p.enable_gamma = preset.enable_gamma; p.enable_curvature = preset.enable_curvature; p.enable_sharper = preset.enable_sharper; shader_backend_->setCrtPiParams(p); } auto Screen::isHardwareAccelerated() const -> bool { return shader_backend_ && shader_backend_->isHardwareAccelerated(); } auto Screen::getActiveShaderName() const -> const char* { if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return "SENSE GPU"; return shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX ? "POSTFX" : "CRT-PI"; } void Screen::updateRenderInfo() { static const Uint32 start_ticks = SDL_GetTicks(); std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_); // Segment 0: FPS + driver (sempre visible) std::string fps_driver = std::to_string(fps_.last_value) + " fps - " + driver; // Segment 1: shader + preset (només si shaders actius) std::string shader_seg; if (Options::video.shader_enabled) { shader_seg = " - " + toLower(getActiveShaderName()) + " " + toLower(getCurrentPresetName()); } // Segment 2: supersampling indicator const char* ss_seg = (Options::video.shader_enabled && Options::video.supersampling) ? " (ss)" : nullptr; // Segment 3: hora (només si show_time) char time_buf[32] = {0}; if (Options::render_info.show_time) { Uint32 elapsed = SDL_GetTicks() - start_ticks; int minutes = elapsed / 60000; int seconds = (elapsed / 1000) % 60; int centis = (elapsed / 10) % 100; snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis); } // Dígits en mono a FPS (segment 0) i TEMPS (segment 3): els dígits canvien // contínuament mentre els símbols del voltant ("fps", ":", ".", " - ") no Overlay::setRenderInfoSegments( fps_driver.c_str(), shader_seg.empty() ? nullptr : shader_seg.c_str(), ss_seg, time_buf[0] ? time_buf : nullptr, 0b1001); } void Screen::applyFallbackPresentation() { // Fallback SDL_Renderer (p.ex. emscripten/WebGL2 sense shaders GPU). // Filtre global (texture_filter) s'aplica sempre, independent de 4:3. SDL_ScaleMode scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR) ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST; if (texture_) SDL_SetTextureScaleMode(texture_, scale); // Si 4:3 actiu, la finestra ja té aspect 4:3 (alçada × 1.2); STRETCH és // l'única opció viable al path fallback (el GPU path fa l'upscale 4:3 abans // d'escollir el mode de finestra; en fallback no tenim eixa capa intermèdia). SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_LETTERBOX; if (Options::video.aspect_ratio_4_3) { mode = SDL_LOGICAL_PRESENTATION_STRETCH; } else { switch (Options::video.scaling_mode) { case Options::ScalingMode::DISABLED: mode = SDL_LOGICAL_PRESENTATION_DISABLED; break; case Options::ScalingMode::STRETCH: mode = SDL_LOGICAL_PRESENTATION_STRETCH; break; case Options::ScalingMode::LETTERBOX: mode = SDL_LOGICAL_PRESENTATION_LETTERBOX; break; case Options::ScalingMode::OVERSCAN: mode = SDL_LOGICAL_PRESENTATION_OVERSCAN; break; case Options::ScalingMode::INTEGER: mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; break; } } // Amb resolució interna N > 1, la mida lògica creix proporcionalment // perquè SDL scale des de 320·N × 200·N a la finestra — menys aggressive linear. const int mult = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution; SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * mult, GAME_HEIGHT * mult, mode); } void Screen::ensureFallbackInternalTexture() { const int mult = Options::video.internal_resolution; if (mult <= 1 || renderer_ == nullptr) { // No cal textura intermèdia. Si la teníem, la reciclem. if (internal_texture_sdl_ != nullptr) { SDL_DestroyTexture(internal_texture_sdl_); internal_texture_sdl_ = nullptr; internal_texture_mult_ = 0; } return; } if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == mult) return; if (internal_texture_sdl_ != nullptr) { SDL_DestroyTexture(internal_texture_sdl_); internal_texture_sdl_ = nullptr; } internal_texture_sdl_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_TARGET, GAME_WIDTH * mult, GAME_HEIGHT * mult); if (internal_texture_sdl_ == nullptr) { std::cerr << "Screen: failed to create fallback internal texture (×" << mult << "): " << SDL_GetError() << '\n'; internal_texture_mult_ = 0; return; } internal_texture_mult_ = mult; } void Screen::adjustWindowSize() { int w = GAME_WIDTH * zoom_; // Si 4:3 actiu, l'alçada visual és 240 per zoom (200 * 1.2) int h = Options::video.aspect_ratio_4_3 ? static_cast(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_; SDL_SetWindowSize(window_, w, h); SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); } void Screen::calculateMaxZoom() { SDL_DisplayID display = SDL_GetPrimaryDisplay(); const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display); if (mode) { int max_w = mode->w / GAME_WIDTH; int max_h = mode->h / GAME_HEIGHT; max_zoom_ = (max_w < max_h) ? max_w : max_h; if (max_zoom_ < 1) max_zoom_ = 1; } }