GlobalInputs F1-F12 + Escape, Screen sense destruir finestra, escalat al GPU, presets i fons canvas (v0.11.0)
This commit is contained in:
1 parent
837eb73398
commit
297dc85e47
23 files changed
+653
-34
No files matched your search
@@ -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
|
||||
|
||||
@@ -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ó)"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,191 @@
|
||||
#include "core/input/global_inputs.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#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<Mode>((static_cast<int>(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<int>(EXIT_WINDOW_S * 1000.0F));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void update(int delta_ms) {
|
||||
exit_window_s = std::max(0.0F, exit_window_s - (static_cast<float>(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
|
||||
@@ -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
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "core/input/key_config.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#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<std::string>();
|
||||
const auto CODE = entry["code"].get_value<std::string>();
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
// 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<std::string, SDL_Scancode> bindings_;
|
||||
};
|
||||
@@ -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<std::uint32_t>(r) << 16) | (static_cast<std::uint32_t>(g) << 8) | static_cast<std::uint32_t>(b);
|
||||
std::ranges::fill(canvas_, COLOR);
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#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{};
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#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};
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#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{};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "core/rendering/sdl3gpu/viewport.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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<float>(swapchain_w);
|
||||
const auto SH = static_cast<float>(swapchain_h);
|
||||
const auto LW = static_cast<float>(logical_w);
|
||||
const auto LH = static_cast<float>(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<int>(SW) / logical_w, static_cast<int>(SH) / logical_h));
|
||||
vw = LW * static_cast<float>(SCALE);
|
||||
vh = LH * static_cast<float>(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
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#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
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#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;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#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");
|
||||
|
||||
+99
-17
@@ -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<Options::PostFXPreset> {
|
||||
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<Options::CrtPiPreset> {
|
||||
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},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#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<float>(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();
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user