#include "core/rendering/screen.h" #include #include // for max, min #include // for lround #include // for memcpy #include // for basic_ostream, operator<<, cout, endl #include // for basic_string, char_traits, string #include "core/input/mouse.hpp" // for Mouse::cursorVisible, Mouse::lastMouseMoveTime #include "core/rendering/text.h" // for Text #include "core/resources/resource.h" #include "game/defaults.hpp" // for GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT #include "game/options.hpp" // for Options::video, Options::settings #ifndef NO_SHADERS #include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // for Rendering::SDL3GPUShader #endif #ifdef __EMSCRIPTEN__ #include #include // --- Fix per a fullscreen/resize en Emscripten --- // // SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN // (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED / // SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de // fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte // però l'estat intern de SDL creu que segueix en fullscreen amb la resolució // anterior i el viewport queda desencuadrat. // // Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick // del event loop (el canvas encara no està estable en el moment del callback) // i cridar setVideoMode() amb el flag de fullscreen actualitzat. La crida // interna a SDL_SetWindowFullscreen(false) és la peça que realment fa eixir // SDL del seu estat intern de fullscreen — sense això res més funciona. namespace { Screen *g_screen_instance = nullptr; void deferredCanvasResize(void * /*userData*/) { if (g_screen_instance) { g_screen_instance->handleCanvasResized(); } } EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent *event, void * /*userData*/) { if (g_screen_instance && event) { g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0); } emscripten_async_call(deferredCanvasResize, nullptr, 0); return EM_FALSE; } EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent * /*event*/, void * /*userData*/) { emscripten_async_call(deferredCanvasResize, nullptr, 0); return EM_FALSE; } } // namespace #endif // __EMSCRIPTEN__ // Instancia única Screen *Screen::instance = nullptr; // Singleton API void Screen::init(SDL_Window *window, SDL_Renderer *renderer) { Screen::instance = new Screen(window, renderer); } void Screen::destroy() { delete Screen::instance; Screen::instance = nullptr; } auto Screen::get() -> Screen * { return Screen::instance; } // Constructor Screen::Screen(SDL_Window *window, SDL_Renderer *renderer) : border_color_{0x00, 0x00, 0x00} { // Inicializa variables this->window_ = window; this->renderer_ = renderer; game_canvas_width_ = GAMECANVAS_WIDTH; game_canvas_height_ = GAMECANVAS_HEIGHT; // Establece el modo de video (fullscreen/ventana + logical presentation) // ANTES de crear la textura — SDL3 GPU necesita la logical presentation // del renderer ya aplicada al swapchain quan es reclama la ventana per a GPU. // Mirror del pattern de jaildoctors_dilemma (que usa exactament 256×192 i // funciona) on `initSDLVideo` configura la presentation abans de crear cap // textura. setVideoMode(Options::video.fullscreen); // Força al window manager a completar el resize/posicionat abans de passar // la ventana al dispositiu GPU. Sense açò en Linux/X11 hi ha un race // condition que deixa el swapchain en estat inestable i fa crashear el // driver Vulkan en `SDL_CreateGPUGraphicsPipeline`. SDL_SyncWindow(window); // Crea la textura donde se dibujan los graficos del juego. // ARGB8888 per simplificar el readback cap al pipeline SDL3 GPU. game_canvas_ = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, game_canvas_width_, game_canvas_height_); if (game_canvas_ != nullptr) { SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode); } if (game_canvas_ == nullptr) { if (Options::settings.console) { std::cout << "gameCanvas could not be created!\nSDL Error: " << SDL_GetError() << '\n'; } } #ifndef NO_SHADERS // Buffer de readback del gameCanvas (lo dimensionamos una vez) pixel_buffer_.resize(static_cast(game_canvas_width_) * static_cast(game_canvas_height_)); #endif // Renderiza una vez la textura vacía al renderer abans d'inicialitzar els // shaders: jaildoctors_dilemma ho fa així i evita que el driver Vulkan // crashegi en la creació del pipeline gràfic. `initShaders()` es crida // després des de `Director` amb el swapchain ja estable. SDL_RenderTexture(renderer, game_canvas_, nullptr, nullptr); // Estado inicial de las notificaciones. El Text real se enlaza después vía // `initNotifications()` quan `Resource` ja estigui inicialitzat. Dividim // això del constructor perquè `initShaders()` (GPU) ha de cridar-se ABANS // de carregar recursos: si el SDL_Renderer ha fet abans moltes // allocacions (carrega de textures), el driver Vulkan crasheja quan // després es reclama la ventana per al dispositiu GPU. notification_text_ = nullptr; notification_message_ = ""; notification_text_color_ = {0xFF, 0xFF, 0xFF}; notification_outline_color_ = {0x00, 0x00, 0x00}; notification_end_time_ = 0; notification_y_ = 2; // Registra callbacks natius d'Emscripten per a fullscreen/orientation registerEmscriptenEventCallbacks(); } // Enllaça el Text de les notificacions amb el recurs compartit de `Resource`. // S'ha de cridar després de `Resource::init(...)`. void Screen::initNotifications() { notification_text_ = Resource::get()->getText("8bithud"); } // Destructor Screen::~Screen() { // notificationText es propiedad de Resource — no liberar. #ifndef NO_SHADERS shutdownShaders(); #endif SDL_DestroyTexture(game_canvas_); } // 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::blit() { // Dibuja la notificación activa sobre el gameCanvas antes de presentar SDL_SetRenderTarget(renderer_, game_canvas_); renderNotification(); #ifndef NO_SHADERS // Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb // shaders com sense). Seguim el mateix pattern que aee_plus: quan shader // està desactivat, forcem POSTFX + params a zero només per a aquest frame // i restaurem el shader actiu, així CRTPI no aplica les seues scanlines // quan l'usuari ho ha desactivat. if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { 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); if (Options::video.shader.enabled) { // Ruta normal: shader amb els seus params. shader_backend_->uploadPixels(pixel_buffer_.data(), game_canvas_width_, game_canvas_height_); shader_backend_->render(); } else { // Shader off: POSTFX amb params zero (passa-per-aquí). CRTPI no // val perque sempre aplica els seus efectes interns; salvem i // restaurem el shader actiu. const auto PREV_SHADER = shader_backend_->getActiveShader(); if (PREV_SHADER != Rendering::ShaderType::POSTFX) { shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX); } shader_backend_->setPostFXParams(Rendering::PostFXParams{}); shader_backend_->uploadPixels(pixel_buffer_.data(), game_canvas_width_, game_canvas_height_); shader_backend_->render(); if (PREV_SHADER != Rendering::ShaderType::POSTFX) { shader_backend_->setActiveShader(PREV_SHADER); } } return; } #endif // Vuelve a dejar el renderizador en modo normal SDL_SetRenderTarget(renderer_, nullptr); // Borra el contenido previo SDL_SetRenderDrawColor(renderer_, border_color_.r, border_color_.g, border_color_.b, 0xFF); SDL_RenderClear(renderer_); // Copia la textura de juego en el renderizador en la posición adecuada SDL_FRect fdest = {(float)dest_.x, (float)dest_.y, (float)dest_.w, (float)dest_.h}; SDL_RenderTexture(renderer_, game_canvas_, nullptr, &fdest); // Muestra por pantalla el renderizador SDL_RenderPresent(renderer_); } // ============================================================================ // Video y ventana // ============================================================================ // Establece el modo de video void Screen::setVideoMode(bool fullscreen) { applyFullscreen(fullscreen); if (fullscreen) { applyFullscreenLayout(); } else { applyWindowedLayout(); } applyLogicalPresentation(fullscreen); // En SDL3 + Vulkan sobre Windows, després de SDL_SetWindowSize la render- // target texture (gameCanvas) queda en un estat on SDL_RenderClear funciona // però SDL_RenderTexture* no dibuixa res: el frame següent només mostra el // fons net, els sprites desapareixen. Title se'n surt sense voler perquè // createTiledBackground() crea/destrueix una textura target nova, i això // reinicialitza l'estat intern del renderer. Recreem gameCanvas aquí // mateix per garantir el mateix efecte en qualsevol escena. if (game_canvas_ != nullptr) { SDL_DestroyTexture(game_canvas_); game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, game_canvas_width_, game_canvas_height_); if (game_canvas_ != nullptr) { SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode); } } } // Cambia entre pantalla completa y ventana void Screen::toggleVideoMode() { setVideoMode(!Options::video.fullscreen); } // Reduce el zoom de la ventana auto Screen::decWindowZoom() -> bool { if (Options::video.fullscreen) { return false; } const int PREV = Options::window.zoom; Options::window.zoom = std::max(Options::window.zoom - 1, WINDOW_ZOOM_MIN); if (Options::window.zoom == PREV) { return false; } setVideoMode(false); return true; } // Aumenta el zoom de la ventana auto Screen::incWindowZoom() -> bool { if (Options::video.fullscreen) { return false; } const int PREV = Options::window.zoom; Options::window.zoom = std::min(Options::window.zoom + 1, Options::window.max_zoom); if (Options::window.zoom == PREV) { return false; } setVideoMode(false); return true; } // Establece el zoom de la ventana directamente auto Screen::setWindowZoom(int zoom) -> bool { if (Options::video.fullscreen) { return false; } if (zoom < WINDOW_ZOOM_MIN || zoom > Options::window.max_zoom) { return false; } if (zoom == Options::window.zoom) { return false; } Options::window.zoom = zoom; setVideoMode(false); return true; } // Detecta el zoom màxim windowed segons la resolució del display actual. void Screen::detectMaxZoom() { #ifdef __EMSCRIPTEN__ // En WASM el tamany del canvas el fixa el browser; el zoom no aplica. return; #else int num_displays = 0; SDL_DisplayID *displays = SDL_GetDisplays(&num_displays); if (displays == nullptr || num_displays == 0) { if (displays != nullptr) { SDL_free(displays); } return; } const auto *dm = SDL_GetCurrentDisplayMode(displays[0]); if (dm != nullptr) { const int MAX_W = dm->w / GAMECANVAS_WIDTH; const int MAX_H = (dm->h - WINDOWS_DECORATIONS) / GAMECANVAS_HEIGHT; const int DETECTED = std::max(WINDOW_ZOOM_MIN, std::min(MAX_W, MAX_H)); Options::window.max_zoom = DETECTED; Options::window.zoom = std::clamp(Options::window.zoom, WINDOW_ZOOM_MIN, DETECTED); if (Options::settings.console) { std::cout << "Display " << dm->w << "x" << dm->h << " → max windowed zoom = " << DETECTED << "x\n"; } } SDL_free(displays); #endif } // Estableix el mode de presentacio del canvas i reaplica el layout void Screen::setPresentationMode(Options::PresentationMode mode) { if (Options::video.presentation_mode == mode) { return; } Options::video.presentation_mode = mode; #ifndef NO_SHADERS if (shader_backend_) { shader_backend_->setPresentationMode(static_cast(mode)); } #endif setVideoMode(Options::video.fullscreen); } // Cicla integer_scale -> letterbox -> stretched -> overscan -> integer_scale void Screen::nextPresentationMode() { setPresentationMode(Options::nextPresentationMode(Options::video.presentation_mode)); } // Nom curt del mode actual (per a notificacions). Static perque no necessita // estat d'instancia: nomes consulta Options::video. auto Screen::getPresentationModeName() -> const char * { return Options::presentationModeToString(Options::video.presentation_mode); } // Establece el V-Sync void Screen::setVSync(bool enabled) { Options::video.vsync = enabled; SDL_SetRenderVSync(renderer_, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED); #ifndef NO_SHADERS if (shader_backend_) { shader_backend_->setVSync(enabled); } #endif } // Alterna el V-Sync void Screen::toggleVSync() { setVSync(!Options::video.vsync); } // Cambia el color del borde void Screen::setBorderColor(Color color) { border_color_ = color; } // ============================================================================ // Helpers privados de setVideoMode // ============================================================================ // SDL_SetWindowFullscreen + visibilidad del cursor void Screen::applyFullscreen(bool fullscreen) { SDL_SetWindowFullscreen(window_, fullscreen); if (fullscreen) { SDL_HideCursor(); Mouse::cursor_visible = false; } else { SDL_ShowCursor(); Mouse::cursor_visible = true; Mouse::last_mouse_move_time = SDL_GetTicks(); } } // Calcula windowWidth/Height/dest para el modo ventana y aplica SDL_SetWindowSize void Screen::applyWindowedLayout() { window_width_ = game_canvas_width_; window_height_ = game_canvas_height_; dest_ = {.x = 0, .y = 0, .w = game_canvas_width_, .h = game_canvas_height_}; #ifdef __EMSCRIPTEN__ windowWidth *= WASM_RENDER_SCALE; windowHeight *= WASM_RENDER_SCALE; dest.w *= WASM_RENDER_SCALE; dest.h *= WASM_RENDER_SCALE; #endif // Modifica el tamaño de la ventana SDL_SetWindowSize(window_, window_width_ * Options::window.zoom, window_height_ * Options::window.zoom); // Sense aquesta sincronia, en Windows + Vulkan el swapchain del SDL3 GPU // es queda en estat out-of-date després del resize i SDL_AcquireGPU- // SwapchainTexture deixa de tornar una textura vàlida → finestra negra. // En Linux Mesa el driver ho tolera, però el patró segur (igual que // jaildoctors_dilemma) és esperar que el WM completi el resize abans de // reposicionar i continuar amb el render. SDL_SyncWindow(window_); SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); } // Obtiene el tamaño de la ventana en fullscreen y calcula el rect del juego void Screen::applyFullscreenLayout() { SDL_GetWindowSize(window_, &window_width_, &window_height_); computeFullscreenGameRect(); } // Calcula el rectangle dest segons el PresentationMode actiu. // INTEGER_SCALE: x sencera maxima (1x, 2x, 3x...) centrada amb barres. // LETTERBOX: mante aspect ratio, ajusta al menor dels eixos, barres. // STRETCHED: omple tota la finestra deformant la relacio d'aspecte. // OVERSCAN: mante aspect ratio omplint la finestra (retalla el sobrant). void Screen::computeFullscreenGameRect() { const float CANVAS_RATIO = static_cast(game_canvas_width_) / static_cast(game_canvas_height_); const float WINDOW_RATIO = static_cast(window_width_) / static_cast(window_height_); switch (Options::video.presentation_mode) { case Options::PresentationMode::INTEGER_SCALE: { int scale = 0; while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) { scale++; } dest_.w = game_canvas_width_ * scale; dest_.h = game_canvas_height_ * scale; break; } case Options::PresentationMode::LETTERBOX: { if (WINDOW_RATIO >= CANVAS_RATIO) { dest_.h = window_height_; dest_.w = static_cast(std::lround(window_height_ * CANVAS_RATIO)); } else { dest_.w = window_width_; dest_.h = static_cast(std::lround(window_width_ / CANVAS_RATIO)); } break; } case Options::PresentationMode::STRETCHED: { dest_.w = window_width_; dest_.h = window_height_; break; } case Options::PresentationMode::OVERSCAN: { // Mante aspect: dimensiona al major dels eixos (l'altre desborda). if (WINDOW_RATIO >= CANVAS_RATIO) { dest_.w = window_width_; dest_.h = static_cast(std::lround(window_width_ / CANVAS_RATIO)); } else { dest_.h = window_height_; dest_.w = static_cast(std::lround(window_height_ * CANVAS_RATIO)); } break; } } dest_.x = (window_width_ - dest_.w) / 2; dest_.y = (window_height_ - dest_.h) / 2; } // Aplica la logical presentation segons el PresentationMode actiu (ruta SDL_Renderer fallback). // La ruta GPU calcula el viewport ella mateixa via computeViewport(). void Screen::applyLogicalPresentation(bool fullscreen) { SDL_RendererLogicalPresentation lp = SDL_LOGICAL_PRESENTATION_LETTERBOX; switch (Options::video.presentation_mode) { case Options::PresentationMode::INTEGER_SCALE: lp = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; break; case Options::PresentationMode::LETTERBOX: lp = SDL_LOGICAL_PRESENTATION_LETTERBOX; break; case Options::PresentationMode::STRETCHED: lp = SDL_LOGICAL_PRESENTATION_STRETCH; break; case Options::PresentationMode::OVERSCAN: lp = SDL_LOGICAL_PRESENTATION_OVERSCAN; break; } SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, lp); Options::video.fullscreen = fullscreen; } // ============================================================================ // Notificaciones // ============================================================================ // Muestra una notificación en la línea superior durante durationMs void Screen::notify(const std::string &text, Color text_color, Color outline_color, Uint32 duration_ms) { notification_message_ = text; notification_text_color_ = text_color; notification_outline_color_ = outline_color; notification_end_time_ = SDL_GetTicks() + duration_ms; } // Limpia la notificación actual void Screen::clearNotification() { notification_end_time_ = 0; notification_message_.clear(); } // Dibuja la notificación activa (si la hay) sobre el gameCanvas void Screen::renderNotification() { if (notification_text_ == nullptr || SDL_GetTicks() >= notification_end_time_) { return; } notification_text_->writeDX(Text::FLAG_CENTER | Text::FLAG_COLOR | Text::FLAG_STROKE, game_canvas_width_ / 2, notification_y_, notification_message_, 1, notification_text_color_, 1, notification_outline_color_); } // ============================================================================ // Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al // principi del fitxer i l'anonymous namespace amb els callbacks natius). // ============================================================================ void Screen::handleCanvasResized() { #ifdef __EMSCRIPTEN__ // La crida a SDL_SetWindowFullscreen + SDL_SetRenderLogicalPresentation // que fa setVideoMode és l'única manera de resincronitzar l'estat intern // de SDL amb el canvas HTML real. setVideoMode(Options::video.fullscreen); #endif } void Screen::syncFullscreenFlagFromBrowser(bool is_fullscreen) { #ifdef __EMSCRIPTEN__ Options::video.fullscreen = isFullscreen; #else (void)is_fullscreen; #endif } void Screen::registerEmscriptenEventCallbacks() { #ifdef __EMSCRIPTEN__ // IMPORTANT: NO registrem resize callback. En mòbil, fer scroll fa que el // navegador oculti/mostri la barra d'URL, disparant un resize del DOM per // cada scroll. Això portava a cridar setVideoMode per cada scroll, que // re-aplicava la logical presentation i corrompia el viewport intern de SDL. g_screen_instance = this; emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange); emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange); #endif } // ============================================================================ // GPU / shaders (SDL3 GPU post-procesado). En builds con NO_SHADERS (Emscripten) // las operaciones son no-op; la ruta clásica sigue siendo la única disponible. // ============================================================================ #ifndef NO_SHADERS // Aplica al backend el shader actiu + els seus presets PostFX i CrtPi. // Només s'ha de cridar quan `videoShaderEnabled=true` (en cas contrari el // blit() ja força POSTFX+zero params per a desactivar els efectes sense // tocar els paràmetres emmagatzemats). void Screen::applyShaderParams() { if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; } shader_backend_->setActiveShader(Options::video.shader.current_shader); applyCurrentPostFXPreset(); applyCurrentCrtPiPreset(); } #endif void Screen::initShaders() { #ifndef NO_SHADERS if (!shader_backend_) { shader_backend_ = std::make_unique(); const std::string FALLBACK_DRIVER = "none"; shader_backend_->setPreferredDriver( Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER); } if (!shader_backend_->isHardwareAccelerated()) { const bool OK = shader_backend_->init(window_, game_canvas_, "", ""); if (Options::settings.console) { std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (OK ? "OK" : "FAILED") << '\n'; } } if (shader_backend_->isHardwareAccelerated()) { shader_backend_->setPresentationMode(static_cast(Options::video.presentation_mode)); shader_backend_->setVSync(Options::video.vsync); // Resol els índexs de preset a partir del nom emmagatzemat al config. // Si el nom no existeix (preset esborrat del YAML), es queda en 0. for (int i = 0; i < static_cast(Options::postfx_presets.size()); ++i) { if (Options::postfx_presets[i].name == Options::video.shader.current_postfx_preset_name) { 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.shader.current_crtpi_preset_name) { Options::current_crtpi_preset = i; break; } } applyShaderParams(); // aplica preset del shader actiu } #endif } void Screen::shutdownShaders() { #ifndef NO_SHADERS // Només es crida des del destructor de Screen. Els toggles runtime NO la // poden cridar: destruir + recrear el dispositiu SDL3 GPU amb la ventana // ja reclamada és inestable (Vulkan/Radeon crasheja en el següent claim). if (shader_backend_) { shader_backend_->cleanup(); shader_backend_.reset(); } #endif } auto Screen::isGpuAccelerated() const -> bool { #ifndef NO_SHADERS return shader_backend_ && shader_backend_->isHardwareAccelerated(); #else return false; #endif } void Screen::setShaderEnabled(bool enabled) { if (Options::video.shader.enabled == enabled) { return; } Options::video.shader.enabled = enabled; #ifndef NO_SHADERS if (enabled) { applyShaderParams(); // restaura preset del shader actiu } // Si enabled=false, blit() forçarà POSTFX+zero per frame — no cal tocar // res ara. #endif } void Screen::toggleShaderEnabled() { setShaderEnabled(!Options::video.shader.enabled); } auto Screen::isShaderEnabled() -> bool { return Options::video.shader.enabled; } #ifndef NO_SHADERS void Screen::setActiveShader(Rendering::ShaderType type) { Options::video.shader.current_shader = type; if (Options::video.shader.enabled) { applyShaderParams(); } } auto Screen::getActiveShader() -> Rendering::ShaderType { return Options::video.shader.current_shader; } #endif void Screen::toggleActiveShader() { #ifndef NO_SHADERS const Rendering::ShaderType NEXT = getActiveShader() == Rendering::ShaderType::POSTFX ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX; setActiveShader(NEXT); #else Options::video.shader.current_shader = Options::video.shader.current_shader == Rendering::ShaderType::POSTFX ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX; #endif } // ============================================================================ // Presets de shaders // ============================================================================ void Screen::applyCurrentPostFXPreset() { #ifndef NO_SHADERS if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; } if (Options::postfx_presets.empty()) { return; } if (Options::current_postfx_preset < 0 || Options::current_postfx_preset >= static_cast(Options::postfx_presets.size())) { Options::current_postfx_preset = 0; } const auto &preset = Options::postfx_presets[Options::current_postfx_preset]; Rendering::PostFXParams p; p.vignette = preset.vignette; p.scanlines = preset.scanlines; p.chroma_min = preset.chroma_min; p.chroma_max = preset.chroma_max; p.mask = preset.mask; p.gamma = preset.gamma; p.curvature = preset.curvature; p.bleeding = preset.bleeding; p.flicker = preset.flicker; p.scan_dark_ratio = preset.scan_dark_ratio; p.scan_dark_floor = preset.scan_dark_floor; p.scan_edge_soft = preset.scan_edge_soft; shader_backend_->setPostFXParams(p); #endif } void Screen::applyCurrentCrtPiPreset() { #ifndef NO_SHADERS if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; } if (Options::crtpi_presets.empty()) { return; } if (Options::current_crtpi_preset < 0 || Options::current_crtpi_preset >= static_cast(Options::crtpi_presets.size())) { Options::current_crtpi_preset = 0; } 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); #endif } auto Screen::getCurrentPresetName() const -> const char * { #ifndef NO_SHADERS if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return "---"; } if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) { if (Options::current_postfx_preset >= 0 && 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 >= 0 && Options::current_crtpi_preset < static_cast(Options::crtpi_presets.size())) { return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str(); } } #endif return "---"; } auto Screen::nextPreset() -> bool { #ifndef NO_SHADERS if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return false; } if (!Options::video.shader.enabled) { return false; } if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) { if (Options::postfx_presets.empty()) { return false; } const int N = static_cast(Options::postfx_presets.size()); Options::current_postfx_preset = (Options::current_postfx_preset + 1) % N; Options::video.shader.current_postfx_preset_name = Options::postfx_presets[Options::current_postfx_preset].name; applyCurrentPostFXPreset(); } else { if (Options::crtpi_presets.empty()) { return false; } const int N = static_cast(Options::crtpi_presets.size()); Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % N; Options::video.shader.current_crtpi_preset_name = Options::crtpi_presets[Options::current_crtpi_preset].name; applyCurrentCrtPiPreset(); } return true; #else return false; #endif } auto Screen::prevPreset() -> bool { #ifndef NO_SHADERS if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return false; } if (!Options::video.shader.enabled) { return false; } if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) { if (Options::postfx_presets.empty()) { return false; } const int N = static_cast(Options::postfx_presets.size()); Options::current_postfx_preset = (Options::current_postfx_preset - 1 + N) % N; Options::video.shader.current_postfx_preset_name = Options::postfx_presets[Options::current_postfx_preset].name; applyCurrentPostFXPreset(); } else { if (Options::crtpi_presets.empty()) { return false; } const int N = static_cast(Options::crtpi_presets.size()); Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + N) % N; Options::video.shader.current_crtpi_preset_name = Options::crtpi_presets[Options::current_crtpi_preset].name; applyCurrentCrtPiPreset(); } return true; #else return false; #endif }