588 lines
22 KiB
C++
588 lines
22 KiB
C++
#include "core/rendering/screen.h"
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <algorithm> // for max, min
|
||
#include <cstring> // for memcpy
|
||
#include <iostream> // for basic_ostream, operator<<, cout, endl
|
||
#include <string> // for basic_string, char_traits, string
|
||
|
||
#include "core/input/mouse.hpp" // for Mouse::cursorVisible, Mouse::lastMouseMoveTime
|
||
#include "core/rendering/text.h" // for Text, TXT_CENTER, TXT_COLOR, TXT_STROKE
|
||
#include "core/resources/asset.h" // for Asset
|
||
#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 <emscripten.h>
|
||
#include <emscripten/html5.h>
|
||
|
||
// --- 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__
|
||
|
||
// Constructor
|
||
Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset) {
|
||
// Inicializa variables
|
||
this->window = window;
|
||
this->renderer = renderer;
|
||
this->asset = asset;
|
||
|
||
gameCanvasWidth = GAMECANVAS_WIDTH;
|
||
gameCanvasHeight = GAMECANVAS_HEIGHT;
|
||
|
||
// Define el color del borde para el modo de pantalla completa
|
||
borderColor = {0x00, 0x00, 0x00};
|
||
|
||
// 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.
|
||
gameCanvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, gameCanvasWidth, gameCanvasHeight);
|
||
if (gameCanvas != nullptr) {
|
||
SDL_SetTextureScaleMode(gameCanvas, Options::video.scale_mode);
|
||
}
|
||
if (gameCanvas == nullptr) {
|
||
if (Options::settings.console) {
|
||
std::cout << "gameCanvas could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
|
||
}
|
||
}
|
||
|
||
#ifndef NO_SHADERS
|
||
// Buffer de readback del gameCanvas (lo dimensionamos una vez)
|
||
pixel_buffer_.resize(static_cast<size_t>(gameCanvasWidth) * static_cast<size_t>(gameCanvasHeight));
|
||
#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, gameCanvas, 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.
|
||
notificationText = nullptr;
|
||
notificationMessage = "";
|
||
notificationTextColor = {0xFF, 0xFF, 0xFF};
|
||
notificationOutlineColor = {0x00, 0x00, 0x00};
|
||
notificationEndTime = 0;
|
||
notificationY = 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() {
|
||
notificationText = Resource::get()->getText("8bithud");
|
||
}
|
||
|
||
// Destructor
|
||
Screen::~Screen() {
|
||
// notificationText es propiedad de Resource — no liberar.
|
||
#ifndef NO_SHADERS
|
||
shutdownShaders();
|
||
#endif
|
||
SDL_DestroyTexture(gameCanvas);
|
||
}
|
||
|
||
// Limpia la pantalla
|
||
void Screen::clean(color_t 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, gameCanvas);
|
||
}
|
||
|
||
// Vuelca el contenido del renderizador en pantalla
|
||
void Screen::blit() {
|
||
// Dibuja la notificación activa sobre el gameCanvas antes de presentar
|
||
SDL_SetRenderTarget(renderer, gameCanvas);
|
||
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(), gameCanvasWidth, gameCanvasHeight);
|
||
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(), gameCanvasWidth, gameCanvasHeight);
|
||
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, borderColor.r, borderColor.g, borderColor.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, gameCanvas, 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);
|
||
}
|
||
|
||
// 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, WINDOW_ZOOM_MAX);
|
||
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 > WINDOW_ZOOM_MAX) { return false; }
|
||
if (zoom == Options::window.zoom) { return false; }
|
||
Options::window.zoom = zoom;
|
||
setVideoMode(false);
|
||
return true;
|
||
}
|
||
|
||
// Establece el escalado entero
|
||
void Screen::setIntegerScale(bool enabled) {
|
||
if (Options::video.integer_scale == enabled) { return; }
|
||
Options::video.integer_scale = enabled;
|
||
setVideoMode(Options::video.fullscreen);
|
||
}
|
||
|
||
// Alterna el escalado entero
|
||
void Screen::toggleIntegerScale() {
|
||
setIntegerScale(!Options::video.integer_scale);
|
||
}
|
||
|
||
// 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_t color) {
|
||
borderColor = color;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Helpers privados de setVideoMode
|
||
// ============================================================================
|
||
|
||
// SDL_SetWindowFullscreen + visibilidad del cursor
|
||
void Screen::applyFullscreen(bool fullscreen) {
|
||
SDL_SetWindowFullscreen(window, fullscreen);
|
||
if (fullscreen) {
|
||
SDL_HideCursor();
|
||
Mouse::cursorVisible = false;
|
||
} else {
|
||
SDL_ShowCursor();
|
||
Mouse::cursorVisible = true;
|
||
Mouse::lastMouseMoveTime = SDL_GetTicks();
|
||
}
|
||
}
|
||
|
||
// Calcula windowWidth/Height/dest para el modo ventana y aplica SDL_SetWindowSize
|
||
void Screen::applyWindowedLayout() {
|
||
windowWidth = gameCanvasWidth;
|
||
windowHeight = gameCanvasHeight;
|
||
dest = {0, 0, gameCanvasWidth, gameCanvasHeight};
|
||
|
||
#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, windowWidth * Options::window.zoom, windowHeight * Options::window.zoom);
|
||
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, &windowWidth, &windowHeight);
|
||
computeFullscreenGameRect();
|
||
}
|
||
|
||
// Calcula el rectángulo dest para fullscreen: integer_scale / aspect ratio
|
||
void Screen::computeFullscreenGameRect() {
|
||
if (Options::video.integer_scale) {
|
||
// Calcula el tamaño de la escala máxima
|
||
int scale = 0;
|
||
while (((gameCanvasWidth * (scale + 1)) <= windowWidth) && ((gameCanvasHeight * (scale + 1)) <= windowHeight)) {
|
||
scale++;
|
||
}
|
||
|
||
dest.w = gameCanvasWidth * scale;
|
||
dest.h = gameCanvasHeight * scale;
|
||
dest.x = (windowWidth - dest.w) / 2;
|
||
dest.y = (windowHeight - dest.h) / 2;
|
||
} else {
|
||
// Manté la relació d'aspecte sense escalat enter (letterbox/pillarbox).
|
||
float ratio = (float)gameCanvasWidth / (float)gameCanvasHeight;
|
||
if ((windowWidth - gameCanvasWidth) >= (windowHeight - gameCanvasHeight)) {
|
||
dest.h = windowHeight;
|
||
dest.w = (int)((windowHeight * ratio) + 0.5f);
|
||
dest.x = (windowWidth - dest.w) / 2;
|
||
dest.y = (windowHeight - dest.h) / 2;
|
||
} else {
|
||
dest.w = windowWidth;
|
||
dest.h = (int)((windowWidth / ratio) + 0.5f);
|
||
dest.x = (windowWidth - dest.w) / 2;
|
||
dest.y = (windowHeight - dest.h) / 2;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Aplica la logical presentation y persiste el estado en options
|
||
void Screen::applyLogicalPresentation(bool fullscreen) {
|
||
SDL_SetRenderLogicalPresentation(renderer, windowWidth, windowHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||
Options::video.fullscreen = fullscreen;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Notificaciones
|
||
// ============================================================================
|
||
|
||
// Muestra una notificación en la línea superior durante durationMs
|
||
void Screen::notify(const std::string &text, color_t textColor, color_t outlineColor, Uint32 durationMs) {
|
||
notificationMessage = text;
|
||
notificationTextColor = textColor;
|
||
notificationOutlineColor = outlineColor;
|
||
notificationEndTime = SDL_GetTicks() + durationMs;
|
||
}
|
||
|
||
// Limpia la notificación actual
|
||
void Screen::clearNotification() {
|
||
notificationEndTime = 0;
|
||
notificationMessage.clear();
|
||
}
|
||
|
||
// Dibuja la notificación activa (si la hay) sobre el gameCanvas
|
||
void Screen::renderNotification() {
|
||
if (notificationText == nullptr || SDL_GetTicks() >= notificationEndTime) {
|
||
return;
|
||
}
|
||
notificationText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE,
|
||
gameCanvasWidth / 2,
|
||
notificationY,
|
||
notificationMessage,
|
||
1,
|
||
notificationTextColor,
|
||
1,
|
||
notificationOutlineColor);
|
||
}
|
||
|
||
// ============================================================================
|
||
// 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 isFullscreen) {
|
||
#ifdef __EMSCRIPTEN__
|
||
Options::video.fullscreen = isFullscreen;
|
||
#else
|
||
(void)isFullscreen;
|
||
#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 preset del shader actiu segons options.
|
||
// 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);
|
||
|
||
// Preset per defecte (carregador YAML pendent). Valors estil "CRT" de CCAE.
|
||
Rendering::PostFXParams POSTFX;
|
||
POSTFX.vignette = 0.15F;
|
||
POSTFX.scanlines = 0.7F;
|
||
POSTFX.chroma = 0.2F;
|
||
shader_backend_->setPostFXParams(POSTFX);
|
||
|
||
// CrtPi: defaults del struct ja raonables (scanline_weight=6.0, bloom=3.5…).
|
||
shader_backend_->setCrtPiParams(Rendering::CrtPiParams{});
|
||
}
|
||
#endif
|
||
|
||
void Screen::initShaders() {
|
||
#ifndef NO_SHADERS
|
||
if (!shader_backend_) {
|
||
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||
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, gameCanvas, "", "");
|
||
if (Options::settings.console) {
|
||
std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (ok ? "OK" : "FAILED") << '\n';
|
||
}
|
||
}
|
||
if (shader_backend_->isHardwareAccelerated()) {
|
||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
||
shader_backend_->setVSync(Options::video.vsync);
|
||
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
|
||
}
|
||
|
||
void Screen::setGpuAcceleration(bool enabled) {
|
||
if (Options::video.gpu.acceleration == enabled) { return; }
|
||
Options::video.gpu.acceleration = enabled;
|
||
// Soft toggle: el backend es manté viu (vegeu shutdownShaders). El canvi
|
||
// s'aplica al proper arrencada. S'emet una notificació perquè l'usuari
|
||
// sap que ha tocat la tecla però el canvi no és immediat.
|
||
const color_t YELLOW = {0xFF, 0xFF, 0x00};
|
||
const color_t BLACK = {0x00, 0x00, 0x00};
|
||
const Uint32 DUR_MS = 2500;
|
||
notify(enabled ? "GPU: ON (restart)" : "GPU: OFF (restart)", YELLOW, BLACK, DUR_MS);
|
||
}
|
||
|
||
void Screen::toggleGpuAcceleration() {
|
||
setGpuAcceleration(!Options::video.gpu.acceleration);
|
||
}
|
||
|
||
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
|
||
const color_t CYAN = {0x00, 0xFF, 0xFF};
|
||
const color_t BLACK = {0x00, 0x00, 0x00};
|
||
const Uint32 DUR_MS = 1500;
|
||
notify(enabled ? "Shader: ON" : "Shader: OFF", CYAN, BLACK, DUR_MS);
|
||
}
|
||
|
||
void Screen::toggleShaderEnabled() {
|
||
setShaderEnabled(!Options::video.shader.enabled);
|
||
}
|
||
|
||
auto Screen::isShaderEnabled() const -> 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();
|
||
}
|
||
const color_t MAGENTA = {0xFF, 0x00, 0xFF};
|
||
const color_t BLACK = {0x00, 0x00, 0x00};
|
||
const Uint32 DUR_MS = 1500;
|
||
notify(type == Rendering::ShaderType::CRTPI ? "Shader: CRTPI" : "Shader: POSTFX", MAGENTA, BLACK, DUR_MS);
|
||
}
|
||
|
||
auto Screen::getActiveShader() const -> 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
|
||
}
|