#include "core/rendering/screen.h" #include #include // for max, min #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, TXT_CENTER, TXT_COLOR, TXT_STROKE #include "core/resources/asset.h" // for Asset #include "core/resources/resource.h" #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__ // Constructor Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options_t *options) { // Inicializa variables this->window = window; this->renderer = renderer; this->options = options; this->asset = asset; gameCanvasWidth = options->gameWidth; gameCanvasHeight = options->gameHeight; borderWidth = options->borderWidth * 2; borderHeight = options->borderHeight * 2; // 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->videoMode != 0); // 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->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR); } if (gameCanvas == nullptr) { if (options->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(gameCanvasWidth) * static_cast(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->videoShaderEnabled) { // 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->videoMode == 0); } // Reduce el zoom de la ventana auto Screen::decWindowZoom() -> bool { if (options->videoMode != 0) { return false; } const int PREV = options->windowSize; options->windowSize = std::max(options->windowSize - 1, WINDOW_ZOOM_MIN); if (options->windowSize == PREV) { return false; } setVideoMode(false); return true; } // Aumenta el zoom de la ventana auto Screen::incWindowZoom() -> bool { if (options->videoMode != 0) { return false; } const int PREV = options->windowSize; options->windowSize = std::min(options->windowSize + 1, WINDOW_ZOOM_MAX); if (options->windowSize == PREV) { return false; } setVideoMode(false); return true; } // Establece el zoom de la ventana directamente auto Screen::setWindowZoom(int zoom) -> bool { if (options->videoMode != 0) { return false; } if (zoom < WINDOW_ZOOM_MIN || zoom > WINDOW_ZOOM_MAX) { return false; } if (zoom == options->windowSize) { return false; } options->windowSize = zoom; setVideoMode(false); return true; } // Establece el escalado entero void Screen::setIntegerScale(bool enabled) { if (options->integerScale == enabled) { return; } options->integerScale = enabled; setVideoMode(options->videoMode != 0); } // Alterna el escalado entero void Screen::toggleIntegerScale() { setIntegerScale(!options->integerScale); } // Establece el V-Sync void Screen::setVSync(bool enabled) { options->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->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() { if (options->borderEnabled) { windowWidth = gameCanvasWidth + borderWidth; windowHeight = gameCanvasHeight + borderHeight; dest = {0 + (borderWidth / 2), 0 + (borderHeight / 2), gameCanvasWidth, gameCanvasHeight}; } else { 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->windowSize, windowHeight * options->windowSize); 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: integerScale / keepAspect / stretched void Screen::computeFullscreenGameRect() { if (options->integerScale) { // 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 if (options->keepAspect) { 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; } } else { dest.w = windowWidth; dest.h = windowHeight; dest.x = dest.y = 0; } } // Aplica la logical presentation y persiste el estado en options void Screen::applyLogicalPresentation(bool fullscreen) { SDL_SetRenderLogicalPresentation(renderer, windowWidth, windowHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX); // Actualiza las opciones options->videoMode = fullscreen ? SDL_WINDOW_FULLSCREEN : 0; options->screen.windowWidth = windowWidth; options->screen.windowHeight = windowHeight; } // ============================================================================ // 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->videoMode != 0); #endif } void Screen::syncFullscreenFlagFromBrowser(bool isFullscreen) { #ifdef __EMSCRIPTEN__ options->videoMode = isFullscreen ? SDL_WINDOW_FULLSCREEN : 0; #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; } const Rendering::ShaderType ACTIVE = options->videoShaderType == 1 ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX; shader_backend_->setActiveShader(ACTIVE); // 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(); const std::string FALLBACK_DRIVER = "none"; shader_backend_->setPreferredDriver( options->videoGpuAcceleration ? options->videoGpuPreferredDriver : FALLBACK_DRIVER); } if (!shader_backend_->isHardwareAccelerated()) { const bool ok = shader_backend_->init(window, gameCanvas, "", ""); if (options->console) { std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (ok ? "OK" : "FAILED") << '\n'; } } if (shader_backend_->isHardwareAccelerated()) { shader_backend_->setScaleMode(options->integerScale); shader_backend_->setVSync(options->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->videoGpuAcceleration == enabled) { return; } options->videoGpuAcceleration = 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->videoGpuAcceleration); } 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->videoShaderEnabled == enabled) { return; } options->videoShaderEnabled = 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->videoShaderEnabled); } auto Screen::isShaderEnabled() const -> bool { return options->videoShaderEnabled; } #ifndef NO_SHADERS void Screen::setActiveShader(Rendering::ShaderType type) { options->videoShaderType = type == Rendering::ShaderType::CRTPI ? 1 : 0; if (options->videoShaderEnabled) { 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->videoShaderType == 1 ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX; } #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->videoShaderType = options->videoShaderType == 1 ? 0 : 1; #endif }