From 297dc85e47ac21edb6f0fdde0e35466916d96aec Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 19 Apr 2026 14:27:16 +0200 Subject: [PATCH] GlobalInputs F1-F12 + Escape, Screen sense destruir finestra, escalat al GPU, presets i fons canvas (v0.11.0) --- CMakeLists.txt | 3 + data/input/keys.yaml | 37 ++++ data/locale/ca.yaml | 17 ++ data/locale/en.yaml | 17 ++ source/core/input/global_inputs.cpp | 191 ++++++++++++++++++ source/core/input/global_inputs.hpp | 14 ++ source/core/input/key_config.cpp | 48 +++++ source/core/input/key_config.hpp | 31 +++ source/core/rendering/screen.cpp | 58 ++++++ source/core/rendering/screen.hpp | 13 ++ .../sdl3gpu/pipelines/crtpi_pipeline.cpp | 12 +- .../sdl3gpu/pipelines/crtpi_pipeline.hpp | 7 +- .../core/rendering/sdl3gpu/sdl3gpu_shader.cpp | 7 +- .../core/rendering/sdl3gpu/sdl3gpu_shader.hpp | 2 + source/core/rendering/sdl3gpu/viewport.cpp | 54 +++++ source/core/rendering/sdl3gpu/viewport.hpp | 21 ++ source/core/rendering/shader_backend.hpp | 6 + source/core/system/director.cpp | 4 + source/game/options.cpp | 116 +++++++++-- source/game/scenes/game_scene.cpp | 16 +- source/game/ui/ui_palette.hpp | 7 +- source/utils/defaults.hpp | 4 +- source/utils/defines.hpp | 2 +- 23 files changed, 653 insertions(+), 34 deletions(-) create mode 100644 data/input/keys.yaml create mode 100644 source/core/input/global_inputs.cpp create mode 100644 source/core/input/global_inputs.hpp create mode 100644 source/core/input/key_config.cpp create mode 100644 source/core/input/key_config.hpp create mode 100644 source/core/rendering/sdl3gpu/viewport.cpp create mode 100644 source/core/rendering/sdl3gpu/viewport.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f3e850..233fce4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,8 @@ set(APP_SOURCES source/main.cpp source/core/system/director.cpp source/core/locale/locale.cpp + source/core/input/key_config.cpp + source/core/input/global_inputs.cpp source/core/rendering/screen.cpp source/core/rendering/notifier.cpp source/core/rendering/render_info.cpp @@ -44,6 +46,7 @@ set(APP_SOURCES source/core/rendering/sprite/moving_sprite.cpp source/core/rendering/sprite/animated_sprite.cpp source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp + source/core/rendering/sdl3gpu/viewport.cpp source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp source/core/resources/resource_helper.cpp source/core/resources/resource_cache.cpp diff --git a/data/input/keys.yaml b/data/input/keys.yaml new file mode 100644 index 0000000..3402bb0 --- /dev/null +++ b/data/input/keys.yaml @@ -0,0 +1,37 @@ +# AEE 2026 — Tecles d'UI (Global Inputs) +# +# Mapeig id → codi SDL (el que reconeix SDL_GetScancodeFromName). Les tecles +# de moviment del jugador viuen a part (Defaults::KeysGame); ací només hi ha +# hotkeys de servei que consumeix GlobalInputs. + +keys: + - id: dec_zoom + code: "F1" + desc: "Redueix el zoom de la finestra" + - id: inc_zoom + code: "F2" + desc: "Augmenta el zoom de la finestra" + - id: fullscreen + code: "F3" + desc: "Alterna pantalla completa" + - id: toggle_shader + code: "F4" + desc: "Activa / desactiva els shaders" + - id: next_shader + code: "F5" + desc: "Canvia entre PostFX i CRT-Pi" + - id: next_preset + code: "F6" + desc: "Següent preset del shader actiu" + - id: next_scaling_mode + code: "F7" + desc: "Següent mode d'escalat (letterbox, integer, ...)" + - id: toggle_vsync + code: "F8" + desc: "Activa / desactiva VSync" + - id: toggle_render_info + code: "F12" + desc: "Mostra o amaga l'overlay de debug" + - id: exit + code: "Escape" + desc: "Eixir (confirmar amb segona pulsació)" diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 58e52d3..db2d5c7 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -3,3 +3,20 @@ notification: test: "notificacio de prova" + +input: + zoom: "zoom" + fullscreen_on: "pantalla completa" + fullscreen_off: "mode finestra" + shader_on: "shaders activats" + shader_off: "shaders desactivats" + vsync_on: "vsync activat" + vsync_off: "vsync desactivat" + shader: "shader" + preset: "preset" + scaling_disabled: "escalat: sense" + scaling_stretch: "escalat: stretch" + scaling_letterbox: "escalat: letterbox" + scaling_overscan: "escalat: overscan" + scaling_integer: "escalat: enter" + press_again_exit: "prem una altra volta per eixir" diff --git a/data/locale/en.yaml b/data/locale/en.yaml index 86ee3d9..bd101f9 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -3,3 +3,20 @@ notification: test: "test notification" + +input: + zoom: "zoom" + fullscreen_on: "fullscreen" + fullscreen_off: "windowed" + shader_on: "shaders on" + shader_off: "shaders off" + vsync_on: "vsync on" + vsync_off: "vsync off" + shader: "shader" + preset: "preset" + scaling_disabled: "scaling: off" + scaling_stretch: "scaling: stretch" + scaling_letterbox: "scaling: letterbox" + scaling_overscan: "scaling: overscan" + scaling_integer: "scaling: integer" + press_again_exit: "press again to exit" diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp new file mode 100644 index 0000000..acdf12a --- /dev/null +++ b/source/core/input/global_inputs.cpp @@ -0,0 +1,191 @@ +#include "core/input/global_inputs.hpp" + +#include + +#include +#include + +#include "core/input/key_config.hpp" +#include "core/locale/locale.hpp" +#include "core/rendering/notifier.hpp" +#include "core/rendering/render_info.hpp" +#include "core/rendering/screen.hpp" +#include "core/system/event_buffer.hpp" +#include "game/options.hpp" +#include "utils/defaults.hpp" + +namespace GlobalInputs { + + namespace { + + // Finestra per a confirmar l'eixida amb una segona pulsació d'Escape. + constexpr float EXIT_WINDOW_S = 1.5F; + constexpr int MIN_ZOOM = 1; + constexpr int MAX_ZOOM = 6; + + float exit_window_s = 0.0F; + bool quit_requested = false; + + // Canvia uniforms i shader actiu sense recrear la finestra. + void reapplyShader() { + Screen::get().setActiveShader(Options::buildActiveShader()); + Screen::get().applyPostFXParams(Options::buildPostFXParams()); + Screen::get().applyCrtPiParams(Options::buildCrtPiParams()); + } + + void notifyLocale(const std::string& key) { + Notifier::get().notify(Locale::get().text(key)); + } + + void onDecZoom() { + if (Options::window.fullscreen) { return; } + if (Options::window.zoom <= MIN_ZOOM) { return; } + Options::window.zoom--; + Screen::get().setWindowZoom(Options::window.zoom); + Options::saveToFile(); + Notifier::get().notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom)); + } + + void onIncZoom() { + if (Options::window.fullscreen) { return; } + if (Options::window.zoom >= MAX_ZOOM) { return; } + Options::window.zoom++; + Screen::get().setWindowZoom(Options::window.zoom); + Options::saveToFile(); + Notifier::get().notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom)); + } + + void onFullscreen() { + Options::window.fullscreen = !Options::window.fullscreen; + Screen::get().setFullscreen(Options::window.fullscreen); + Options::saveToFile(); + notifyLocale(Options::window.fullscreen ? "input.fullscreen_on" : "input.fullscreen_off"); + } + + void onToggleVSync() { + Options::video.vsync = !Options::video.vsync; + Screen::get().setVSync(Options::video.vsync); + Options::saveToFile(); + notifyLocale(Options::video.vsync ? "input.vsync_on" : "input.vsync_off"); + } + + void onToggleShader() { + Options::video.shader.enabled = !Options::video.shader.enabled; + reapplyShader(); + Options::saveToFile(); + notifyLocale(Options::video.shader.enabled ? "input.shader_on" : "input.shader_off"); + } + + void onNextShader() { + Options::video.shader.current_shader = (Options::video.shader.current_shader == "crtpi") ? "postfx" : "crtpi"; + reapplyShader(); + Options::saveToFile(); + Notifier::get().notify(Locale::get().text("input.shader") + ": " + Options::video.shader.current_shader); + } + + // Busca l'entrada actual al vector de presets i passa a la següent + // (wrap-around). Si no en troba cap s'evita el canvi silenciosament. + void onNextPreset() { + const bool IS_CRTPI = Options::video.shader.current_shader == "crtpi"; + if (IS_CRTPI) { + if (Options::crtpi_presets.empty()) { return; } + auto it = std::ranges::find_if(Options::crtpi_presets, + [](const auto& preset) { return preset.name == Options::video.shader.current_crtpi_preset; }); + std::size_t idx = (it == Options::crtpi_presets.end()) ? 0 : std::distance(Options::crtpi_presets.begin(), it); + idx = (idx + 1) % Options::crtpi_presets.size(); + Options::video.shader.current_crtpi_preset = Options::crtpi_presets[idx].name; + } else { + if (Options::postfx_presets.empty()) { return; } + auto it = std::ranges::find_if(Options::postfx_presets, + [](const auto& preset) { return preset.name == Options::video.shader.current_postfx_preset; }); + std::size_t idx = (it == Options::postfx_presets.end()) ? 0 : std::distance(Options::postfx_presets.begin(), it); + idx = (idx + 1) % Options::postfx_presets.size(); + Options::video.shader.current_postfx_preset = Options::postfx_presets[idx].name; + } + reapplyShader(); + Options::saveToFile(); + const std::string& name = IS_CRTPI ? Options::video.shader.current_crtpi_preset : Options::video.shader.current_postfx_preset; + Notifier::get().notify(Locale::get().text("input.preset") + ": " + name); + } + + void onNextScalingMode() { + using Mode = Defaults::ScalingMode; + Options::video.scaling_mode = static_cast((static_cast(Options::video.scaling_mode) + 1) % 5); + Screen::get().setScalingMode(Options::video.scaling_mode); + Options::saveToFile(); + const char* mode_key = nullptr; + switch (Options::video.scaling_mode) { + case Mode::DISABLED: + mode_key = "input.scaling_disabled"; + break; + case Mode::STRETCH: + mode_key = "input.scaling_stretch"; + break; + case Mode::LETTERBOX: + mode_key = "input.scaling_letterbox"; + break; + case Mode::OVERSCAN: + mode_key = "input.scaling_overscan"; + break; + case Mode::INTEGER: + mode_key = "input.scaling_integer"; + break; + } + notifyLocale(mode_key); + } + + void onToggleRenderInfo() { + RenderInfo::get().toggle(); + } + + void onExit() { + if (exit_window_s > 0.0F) { + quit_requested = true; + return; + } + exit_window_s = EXIT_WINDOW_S; + // La notificació dura el doble de la finestra; si l'usuari no + // prem una segona volta abans del timeout, es desarmarà sol. + Notifier::get().notify(Locale::get().text("input.press_again_exit"), + Notification::DEFAULT.text_color, + Notification::DEFAULT.bg_color, + static_cast(EXIT_WINDOW_S * 1000.0F)); + } + + } // namespace + + void update(int delta_ms) { + exit_window_s = std::max(0.0F, exit_window_s - (static_cast(delta_ms) / 1000.0F)); + + for (const auto& event : EventBuffer::events) { + if (event.type != SDL_EVENT_KEY_DOWN) { continue; } + const auto SC = event.key.scancode; + if (SC == KeyConfig::get().scancode("dec_zoom")) { + onDecZoom(); + } else if (SC == KeyConfig::get().scancode("inc_zoom")) { + onIncZoom(); + } else if (SC == KeyConfig::get().scancode("fullscreen")) { + onFullscreen(); + } else if (SC == KeyConfig::get().scancode("toggle_shader")) { + onToggleShader(); + } else if (SC == KeyConfig::get().scancode("toggle_vsync")) { + onToggleVSync(); + } else if (SC == KeyConfig::get().scancode("next_shader")) { + onNextShader(); + } else if (SC == KeyConfig::get().scancode("next_preset")) { + onNextPreset(); + } else if (SC == KeyConfig::get().scancode("next_scaling_mode")) { + onNextScalingMode(); + } else if (SC == KeyConfig::get().scancode("toggle_render_info")) { + onToggleRenderInfo(); + } else if (SC == KeyConfig::get().scancode("exit")) { + onExit(); + } + } + } + + auto wantsQuit() -> bool { + return quit_requested; + } + +} // namespace GlobalInputs diff --git a/source/core/input/global_inputs.hpp b/source/core/input/global_inputs.hpp new file mode 100644 index 0000000..324c69d --- /dev/null +++ b/source/core/input/global_inputs.hpp @@ -0,0 +1,14 @@ +#pragma once + +// Hotkeys globals d'UI (F1–F12 + Escape). Consumides al principi del update +// de cada escena, així les tecles de sistema mai es barregen amb l'input del +// joc. L'eixida amb Escape és en dos passos: la primera pulsació mostra una +// notificació de confirmació i arma un temporitzador intern; la segona dins +// de la finestra activa wantsQuit(). +namespace GlobalInputs { + + void update(int delta_ms); + + [[nodiscard]] auto wantsQuit() -> bool; + +} // namespace GlobalInputs diff --git a/source/core/input/key_config.cpp b/source/core/input/key_config.cpp new file mode 100644 index 0000000..36e4e38 --- /dev/null +++ b/source/core/input/key_config.cpp @@ -0,0 +1,48 @@ +#include "core/input/key_config.hpp" + +#include +#include +#include + +#include "external/fkyaml_node.hpp" + +auto KeyConfig::get() -> KeyConfig& { + static KeyConfig instance_; + return instance_; +} + +void KeyConfig::load(const std::string& file_path) { + std::ifstream file(file_path); + if (!file.is_open()) { + std::cerr << "KeyConfig: no s'ha pogut obrir " << file_path << '\n'; + return; + } + + try { + auto yaml = fkyaml::node::deserialize(file); + if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) { + std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n'; + return; + } + bindings_.clear(); + for (const auto& entry : yaml["keys"]) { + if (!entry.contains("id") || !entry.contains("code")) { continue; } + const auto ID = entry["id"].get_value(); + const auto CODE = entry["code"].get_value(); + const SDL_Scancode SC = SDL_GetScancodeFromName(CODE.c_str()); + if (SC == SDL_SCANCODE_UNKNOWN) { + std::cerr << "KeyConfig: codi SDL desconegut '" << CODE << "' per id '" << ID << "'\n"; + continue; + } + bindings_[ID] = SC; + } + std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << file_path << '\n'; + } catch (const std::exception& e) { + std::cerr << "KeyConfig: error parsejant " << file_path << ": " << e.what() << '\n'; + } +} + +auto KeyConfig::scancode(const std::string& id) const -> SDL_Scancode { + auto it = bindings_.find(id); + return it != bindings_.end() ? it->second : SDL_SCANCODE_UNKNOWN; +} diff --git a/source/core/input/key_config.hpp b/source/core/input/key_config.hpp new file mode 100644 index 0000000..0690a4f --- /dev/null +++ b/source/core/input/key_config.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include + +// Mapeig id → SDL_Scancode carregat des de data/input/keys.yaml. Consumit +// per GlobalInputs i (en el futur) per menús de remapat. No té cap cost +// global: si la clau no existeix, retorna SDL_SCANCODE_UNKNOWN i GlobalInputs +// ignora l'acció. D'aquesta manera podem afegir nous bindings al YAML sense +// tocar codi mentre els handlers continuen funcionant. +class KeyConfig { + public: + static auto get() -> KeyConfig&; + + KeyConfig(const KeyConfig&) = delete; + KeyConfig(KeyConfig&&) = delete; + auto operator=(const KeyConfig&) -> KeyConfig& = delete; + auto operator=(KeyConfig&&) -> KeyConfig& = delete; + + void load(const std::string& file_path); + + [[nodiscard]] auto scancode(const std::string& id) const -> SDL_Scancode; + + private: + KeyConfig() = default; + ~KeyConfig() = default; + + std::unordered_map bindings_; +}; diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index b9a83c5..82c7c56 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -156,6 +156,64 @@ void Screen::applyCrtPiParams(const Rendering::CrtPiParams& params) { } } +void Screen::setWindowZoom(int zoom) { + config_.zoom = zoom; + adjustWindowSize(); +} + +void Screen::setFullscreen(bool fullscreen) { + config_.fullscreen = fullscreen; + SDL_SetWindowFullscreen(window_, fullscreen); + SDL_SyncWindow(window_); + adjustWindowSize(); +} + +void Screen::setVSync(bool vsync) { + config_.vsync = vsync; + if (shader_backend_ != nullptr) { + shader_backend_->setVSync(vsync); + } else if (renderer_ != nullptr) { + SDL_SetRenderVSync(renderer_, vsync ? 1 : 0); + } +} + +void Screen::setScalingMode(Defaults::ScalingMode mode) { + config_.scaling_mode = mode; + if (shader_backend_ != nullptr) { + shader_backend_->setScalingMode(mode); + } else if (renderer_ != nullptr) { + SDL_SetRenderLogicalPresentation(renderer_, GameCanvas::WIDTH, GameCanvas::HEIGHT, toSdlPresentation(mode)); + } +} + +// Fullscreen manté la resolució del desktop; en mode finestra recalculem i +// reposicionem. A Wayland el compositor ignora SDL_SetWindowPosition, així +// que allí el reset queda en mans del WM. +void Screen::adjustWindowSize() { + if (config_.fullscreen) { return; } + + int old_w = 0; + int old_h = 0; + SDL_GetWindowSize(window_, &old_w, &old_h); + int old_x = 0; + int old_y = 0; + SDL_GetWindowPosition(window_, &old_x, &old_y); + + const int NEW_W = GameCanvas::WIDTH * config_.zoom; + const int NEW_H = GameCanvas::HEIGHT * config_.zoom; + const int NEW_X = old_x + ((old_w - NEW_W) / 2); + const int NEW_Y = old_y + ((old_h - NEW_H) / 2); + + SDL_SetWindowSize(window_, NEW_W, NEW_H); + SDL_SyncWindow(window_); + + const char* driver = SDL_GetCurrentVideoDriver(); + const bool IS_WAYLAND = (driver != nullptr && SDL_strcmp(driver, "wayland") == 0); + if (!IS_WAYLAND) { + SDL_SetWindowPosition(window_, std::max(NEW_X, 0), std::max(NEW_Y, 0)); + } +} + void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) { const std::uint32_t COLOR = (0xFFU << 24) | (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); std::ranges::fill(canvas_, COLOR); diff --git a/source/core/rendering/screen.hpp b/source/core/rendering/screen.hpp index b55ef76..d6ee052 100644 --- a/source/core/rendering/screen.hpp +++ b/source/core/rendering/screen.hpp @@ -54,6 +54,16 @@ class Screen { // Reenvia paràmetres CrtPi al backend GPU. No-op al path fallback. void applyCrtPiParams(const Rendering::CrtPiParams& params); + // Canvis lleugers que NO destrueixen la finestra. Zoom i fullscreen + // redimensionen SDL_Window i mantenen la finestra centrada respecte a + // l'anterior posició; VSync s'envia directament al backend GPU. + void setWindowZoom(int zoom); + void setFullscreen(bool fullscreen); + void setVSync(bool vsync); + // Mode d'escalat del path GPU (letterbox/integer/stretch/...). Al + // fallback SDL_Renderer es propaga via SDL_SetRenderLogicalPresentation. + void setScalingMode(Defaults::ScalingMode mode); + void clear(std::uint8_t r = 0, std::uint8_t g = 0, std::uint8_t b = 0); // Pinta un rectangle opac al canvas ARGB del joc. Pensat per a HUDs // simples (caixes de notificacions, barres d'estat); no gasta paleta. @@ -75,6 +85,9 @@ class Screen { void destroySdl(); void createSdl(); + // Redimensiona la finestra existent i la recenta sense tancar-la; al + // fallback SDL_Renderer també ajusta la presentació lògica. + void adjustWindowSize(); // Comptador intern de frames per segon. S'incrementa a cada present() i // es recalcula quan ha passat un segon complet (SDL_GetTicks en ms). diff --git a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp index f22bc65..63e1114 100644 --- a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp +++ b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp @@ -3,6 +3,8 @@ #include #include +#include "core/rendering/sdl3gpu/viewport.hpp" + #ifndef __APPLE__ #include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h" @@ -280,7 +282,10 @@ namespace Rendering { SDL_GPUTexture* scene, SDL_GPUSampler* sampler, int game_width, - int game_height) { + int game_height, + Defaults::ScalingMode scaling_mode, + std::uint32_t swapchain_w, + std::uint32_t swapchain_h) { if (pipeline_ == nullptr) { return; } SDL_GPUColorTargetInfo color_target{}; @@ -292,6 +297,11 @@ namespace Rendering { SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr); if (pass == nullptr) { return; } + // Viewport per aplicar letterbox/integer/... sense modificar el + // triangle fullscreen; la resta del swapchain queda a clear_color. + const SDL_GPUViewport VP = computeViewport(scaling_mode, swapchain_w, swapchain_h, game_width, game_height); + SDL_SetGPUViewport(pass, &VP); + SDL_BindGPUGraphicsPipeline(pass, pipeline_); SDL_GPUTextureSamplerBinding binding{}; diff --git a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp index 3ced73b..d94a5f6 100644 --- a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp +++ b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp @@ -3,6 +3,8 @@ #include #include +#include + #include "core/rendering/shader_backend.hpp" // Uniforms del shader CRT-Pi. 4 × vec4 (64 bytes) per a alineament Metal/Vulkan. @@ -56,7 +58,10 @@ namespace Rendering { SDL_GPUTexture* scene, SDL_GPUSampler* sampler, int game_width, - int game_height); + int game_height, + Defaults::ScalingMode scaling_mode, + std::uint32_t swapchain_w, + std::uint32_t swapchain_h); private: SDL_GPUDevice* device_{nullptr}; diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index 8cacf38..56ee556 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -4,6 +4,8 @@ #include #include +#include "core/rendering/sdl3gpu/viewport.hpp" + #ifndef __APPLE__ #include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h" @@ -370,7 +372,7 @@ namespace Rendering { // Camí CrtPi: delega tot el render pass al pipeline dedicat. if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) { - crtpi_pipeline_->render(cmd, swapchain, scene_texture_, sampler_, game_width_, game_height_); + crtpi_pipeline_->render(cmd, swapchain, scene_texture_, sampler_, game_width_, game_height_, scaling_mode_, sw, sh); SDL_SubmitGPUCommandBuffer(cmd); return; } @@ -385,6 +387,9 @@ namespace Rendering { SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr); if (pass != nullptr) { + const SDL_GPUViewport VP = computeViewport(scaling_mode_, sw, sh, game_width_, game_height_); + SDL_SetGPUViewport(pass, &VP); + SDL_BindGPUGraphicsPipeline(pass, pipeline_); SDL_GPUTextureSamplerBinding binding{}; diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index 848a139..d1b0fca 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -53,6 +53,7 @@ namespace Rendering { void setActiveShader(ShaderType type) override { active_shader_ = type; } void setCrtPiParams(const CrtPiParams& params) override; void setVSync(bool vsync) override; + void setScalingMode(Defaults::ScalingMode mode) override { scaling_mode_ = mode; } void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; } void cleanup() override; @@ -99,6 +100,7 @@ namespace Rendering { int game_width_{0}; int game_height_{0}; + Defaults::ScalingMode scaling_mode_{Defaults::Video::SCALING_MODE}; std::string preferred_driver_; std::string driver_name_; bool vsync_{true}; diff --git a/source/core/rendering/sdl3gpu/viewport.cpp b/source/core/rendering/sdl3gpu/viewport.cpp new file mode 100644 index 0000000..2d14772 --- /dev/null +++ b/source/core/rendering/sdl3gpu/viewport.cpp @@ -0,0 +1,54 @@ +#include "core/rendering/sdl3gpu/viewport.hpp" + +#include +#include + +namespace Rendering { + + auto computeViewport(Defaults::ScalingMode mode, + std::uint32_t swapchain_w, + std::uint32_t swapchain_h, + int logical_w, + int logical_h) -> SDL_GPUViewport { + const auto SW = static_cast(swapchain_w); + const auto SH = static_cast(swapchain_h); + const auto LW = static_cast(logical_w); + const auto LH = static_cast(logical_h); + + float vw = 0.0F; + float vh = 0.0F; + switch (mode) { + case Defaults::ScalingMode::DISABLED: + vw = LW; + vh = LH; + break; + case Defaults::ScalingMode::STRETCH: + vw = SW; + vh = SH; + break; + case Defaults::ScalingMode::LETTERBOX: { + const float SCALE = std::min(SW / LW, SH / LH); + vw = LW * SCALE; + vh = LH * SCALE; + break; + } + case Defaults::ScalingMode::OVERSCAN: { + const float SCALE = std::max(SW / LW, SH / LH); + vw = LW * SCALE; + vh = LH * SCALE; + break; + } + case Defaults::ScalingMode::INTEGER: { + const int SCALE = std::max(1, + std::min(static_cast(SW) / logical_w, static_cast(SH) / logical_h)); + vw = LW * static_cast(SCALE); + vh = LH * static_cast(SCALE); + break; + } + } + const float VX = std::floor((SW - vw) * 0.5F); + const float VY = std::floor((SH - vh) * 0.5F); + return {.x = VX, .y = VY, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F}; + } + +} // namespace Rendering diff --git a/source/core/rendering/sdl3gpu/viewport.hpp b/source/core/rendering/sdl3gpu/viewport.hpp new file mode 100644 index 0000000..b1c7d9e --- /dev/null +++ b/source/core/rendering/sdl3gpu/viewport.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include + +#include "utils/defaults.hpp" + +namespace Rendering { + + // Calcula el rectangle on s'ha de pintar el triangle fullscreen dins del + // swapchain segons el mode d'escalat configurat. La zona fora del + // viewport queda amb la clear_color del render pass (negre o el color + // que decidisca el caller). + auto computeViewport(Defaults::ScalingMode mode, + std::uint32_t swapchain_w, + std::uint32_t swapchain_h, + int logical_w, + int logical_h) -> SDL_GPUViewport; + +} // namespace Rendering diff --git a/source/core/rendering/shader_backend.hpp b/source/core/rendering/shader_backend.hpp index 48d13a0..16c6786 100644 --- a/source/core/rendering/shader_backend.hpp +++ b/source/core/rendering/shader_backend.hpp @@ -5,6 +5,8 @@ #include #include +#include "utils/defaults.hpp" + namespace Rendering { // Shader de post-procés actiu. El backend escull el pipeline corresponent. @@ -78,6 +80,10 @@ namespace Rendering { // Canvia el mode de presentació (VSYNC/IMMEDIATE/MAILBOX) en calent. virtual void setVSync(bool vsync) = 0; + // Mode d'escalat del triangle fullscreen al swapchain (letterbox, + // integer, overscan, ...). Implementat via SDL_SetGPUViewport. + virtual void setScalingMode(Defaults::ScalingMode mode) = 0; + // Fixa el driver preferit (buit = automàtic). S'ha de cridar abans d'init(). virtual void setPreferredDriver(const std::string& driver) = 0; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index f13c8b5..19e9864 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -10,6 +10,7 @@ #include #include +#include "core/input/key_config.hpp" #include "core/locale/locale.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" @@ -59,6 +60,9 @@ Director::Director() { // o hi ha un error de parse, Locale::text() caurà a retornar la clau. Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale + ".yaml"); + // Hotkeys globals. GlobalInputs::update() les consumirà a cada frame. + KeyConfig::get().load("data/input/keys.yaml"); + // Recursos compartits d'UI (paleta + font). Es carreguen abans que els // overlays perquè tots els reaprofiten sense duplicar la lògica. Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal"); diff --git a/source/game/options.cpp b/source/game/options.cpp index 22034b8..235625a 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -125,11 +125,10 @@ namespace { parseSound(node); } - // Presets per defecte de PostFX, usats quan el fitxer postfx.yaml no existeix. + // Presets per defecte de PostFX, portats directament de projecte_2026. auto defaultPostFXPresets() -> std::vector { return { - {.name = "off"}, // passthrough (tot a 0) - {.name = "crt", + {.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma = 0.15F, @@ -138,15 +137,51 @@ namespace { .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F}, - {.name = "ntsc", + {.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, - .chroma = 0.25F, - .mask = 0.3F, - .gamma = 0.6F, + .chroma = 0.2F, + .mask = 0.4F, + .gamma = 0.5F, .curvature = 0.0F, + .bleeding = 0.6F, + .flicker = 0.0F}, + {.name = "CURVED", + .vignette = 0.5F, + .scanlines = 0.6F, + .chroma = 0.1F, + .mask = 0.5F, + .gamma = 0.7F, + .curvature = 0.8F, + .bleeding = 0.0F, + .flicker = 0.0F}, + {.name = "SCANLINES", + .vignette = 0.0F, + .scanlines = 0.8F, + .chroma = 0.0F, + .mask = 0.0F, + .gamma = 0.0F, + .curvature = 0.0F, + .bleeding = 0.0F, + .flicker = 0.0F}, + {.name = "SUBTLE", + .vignette = 0.3F, + .scanlines = 0.4F, + .chroma = 0.05F, + .mask = 0.0F, + .gamma = 0.3F, + .curvature = 0.0F, + .bleeding = 0.0F, + .flicker = 0.0F}, + {.name = "CRT LIVE", + .vignette = 0.5F, + .scanlines = 0.6F, + .chroma = 0.3F, + .mask = 0.3F, + .gamma = 0.4F, + .curvature = 0.3F, .bleeding = 0.4F, - .flicker = 0.1F}, + .flicker = 0.8F}, }; } @@ -174,22 +209,69 @@ namespace { out << " flicker: " << preset.flicker << "\n"; } - // Presets per defecte de CrtPi (tuning bàsic del shader original CRT-Pi). + // Presets per defecte del shader CRT-Pi, portats de projecte_2026. auto defaultCrtPiPresets() -> std::vector { return { - {.name = "default"}, - {.name = "sharper", - .scanline_weight = 8.0F, - .scanline_gap_brightness = 0.10F, - .bloom_factor = 3.0F, - .enable_sharper = true}, - {.name = "curved", + {.name = "DEFAULT", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, + .input_gamma = 2.4F, + .output_gamma = 2.2F, + .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, - .enable_curvature = true}, + .mask_type = 2, + .enable_scanlines = true, + .enable_multisample = true, + .enable_gamma = true, + .enable_curvature = false, + .enable_sharper = false}, + {.name = "CURVED", + .scanline_weight = 6.0F, + .scanline_gap_brightness = 0.12F, + .bloom_factor = 3.5F, + .input_gamma = 2.4F, + .output_gamma = 2.2F, + .mask_brightness = 0.80F, + .curvature_x = 0.05F, + .curvature_y = 0.10F, + .mask_type = 2, + .enable_scanlines = true, + .enable_multisample = true, + .enable_gamma = true, + .enable_curvature = true, + .enable_sharper = false}, + {.name = "SHARP", + .scanline_weight = 6.0F, + .scanline_gap_brightness = 0.12F, + .bloom_factor = 3.5F, + .input_gamma = 2.4F, + .output_gamma = 2.2F, + .mask_brightness = 0.80F, + .curvature_x = 0.05F, + .curvature_y = 0.10F, + .mask_type = 2, + .enable_scanlines = true, + .enable_multisample = false, + .enable_gamma = true, + .enable_curvature = false, + .enable_sharper = true}, + {.name = "MINIMAL", + .scanline_weight = 8.0F, + .scanline_gap_brightness = 0.05F, + .bloom_factor = 2.0F, + .input_gamma = 2.4F, + .output_gamma = 2.2F, + .mask_brightness = 1.00F, + .curvature_x = 0.0F, + .curvature_y = 0.0F, + .mask_type = 0, + .enable_scanlines = true, + .enable_multisample = false, + .enable_gamma = false, + .enable_curvature = false, + .enable_sharper = false}, }; } diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 9acfa00..c674cc8 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -2,13 +2,13 @@ #include -#include "core/locale/locale.hpp" +#include "core/input/global_inputs.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/resources/resource_cache.hpp" -#include "core/system/event_buffer.hpp" #include "game/entities/player.hpp" +#include "game/ui/ui_palette.hpp" #include "utils/defaults.hpp" #include "utils/defines.hpp" @@ -33,13 +33,8 @@ GameScene::GameScene() { GameScene::~GameScene() = default; void GameScene::update(int delta_ms) { - for (const auto& event : EventBuffer::events) { - if (event.type == SDL_EVENT_KEY_DOWN) { - if (event.key.scancode == SDL_SCANCODE_ESCAPE) { wants_quit_ = true; } - if (event.key.scancode == SDL_SCANCODE_F3) { RenderInfo::get().toggle(); } - if (event.key.scancode == SDL_SCANCODE_F4) { Notifier::get().notify(Locale::get().text("notification.test")); } - } - } + GlobalInputs::update(delta_ms); + if (GlobalInputs::wantsQuit()) { wants_quit_ = true; } const float DT = static_cast(delta_ms) / 1000.0F; for (auto& player : players_) { player->update(DT); } @@ -48,7 +43,8 @@ void GameScene::update(int delta_ms) { } void GameScene::render() { - Screen::get().clear(0x00, 0x00, 0x00); + const auto BG_ARGB = Resource::Cache::get().getPalette("ui")[UiColor::CANVAS_BG]; + Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BG_ARGB); for (auto& player : players_) { player->render(); } Notifier::get().render(); RenderInfo::get().render(); diff --git a/source/game/ui/ui_palette.hpp b/source/game/ui/ui_palette.hpp index be018cd..9220de2 100644 --- a/source/game/ui/ui_palette.hpp +++ b/source/game/ui/ui_palette.hpp @@ -9,6 +9,11 @@ namespace UiColor { // Color per al text de l'overlay de debug RenderInfo (F3). - constexpr Uint8 RENDER_INFO_TEXT = 9; + constexpr Uint8 RENDER_INFO_TEXT = 47; + + // Color de fons del canvas del joc quan no hi ha escena pintada a tota + // la superfície. Útil per a distingir la zona interior del joc en + // escalats que deixen banda negra (letterbox, integer, ...). + constexpr Uint8 CANVAS_BG = 65; } // namespace UiColor diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index ad415e2..06e634f 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -41,8 +41,8 @@ namespace Defaults { constexpr const char* GPU_PREFERRED_DRIVER = ""; // Buit = auto. Aplica a l'arrencada. constexpr bool SHADER_ENABLED = true; // Aplica PostFX; si false, sempre passthrough. constexpr const char* CURRENT_SHADER = "postfx"; // Shader actiu: "postfx" o "crtpi". - constexpr const char* POSTFX_DEFAULT_PRESET = "off"; // Preset inicial PostFX. - constexpr const char* CRTPI_DEFAULT_PRESET = "default"; // Preset inicial CrtPi. + constexpr const char* POSTFX_DEFAULT_PRESET = "CRT"; // Preset inicial PostFX. + constexpr const char* CRTPI_DEFAULT_PRESET = "DEFAULT"; // Preset inicial CrtPi. constexpr const char* POSTFX_FILE = "shaders/postfx.yaml"; // Relatiu a la carpeta de sistema. constexpr const char* CRTPI_FILE = "shaders/crtpi.yaml"; // Relatiu a la carpeta de sistema. constexpr bool RENDER_INFO = false; // Overlay de debug (FPS/driver/shader) amagat per defecte. diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index 6ef9cd3..5c7a6f7 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -3,7 +3,7 @@ namespace Texts { constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; - constexpr const char* VERSION = "0.10.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.11.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas {