From 9243db1b9f030372f370300fbea883862b803eca Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:02:08 +0200 Subject: [PATCH 01/19] docs: afegir full de treball de l'auditoria de core/rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inventari inicial dels hallazgos detectats a source/core/rendering (duplicació, hardcoding, parches, acoblament, inconsistències, codi mort i violacions de convencions), amb ID estable i estat PENDING. S'anirà actualitzant a mesura que s'apliquen els canvis. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 121 +++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/audit-core-rendering.md diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md new file mode 100644 index 0000000..5fc1286 --- /dev/null +++ b/docs/audit-core-rendering.md @@ -0,0 +1,121 @@ +# Auditoría `core/rendering/` + +Rama: `audit/core-rendering` +Estado global: en curso + +Estados posibles: +- `PENDING` — identificado, sin tocar +- `WIP` — diff propuesto, pendiente de aplicar +- `DONE` — aplicado y commiteado +- `WONTFIX` — se decide no tocar (justificación en la nota) +- `DEFERRED` — se hará en fase posterior + +Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fichero(s) y líneas afectados en la rama de origen (`main` en el momento del clone). + +--- + +## Top 5 prioritario + +| ID | Título | Estado | +|----|--------|--------| +| COUP-01 | Inversión de dependencia `core/rendering → game/*` | PENDING | +| DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | PENDING | +| CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | PENDING | +| DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | PENDING | +| COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | PENDING | + +--- + +## 1. Duplicación + +| ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | +|----|---------------|----------|-----------|--------|-------| +| DUP-01 | `gif.cpp:238-276` | 3 ramas del switch extension saltan bloque+subloops idénticos | Importante | PENDING | helper `skipExtensionBlocks` | +| DUP-02 | `surface.cpp:80-137` | `render` vs `renderWithColorReplace` ~90% idénticos | Crítico | PENDING | inner loop compartido | +| DUP-03 | `text.cpp:145-289` | bucle UTF-8 repetido en write/writeColored/writeMonoDigits/length/lengthMonoDigits | Importante | PENDING | iterador de glifos | +| DUP-04 | `sdl3gpu_shader.cpp:165-201` + `crtpi_pipeline.cpp:165-202` | `createShaderMSL/SPIRV` duplicados | Importante | PENDING | extraer a `shader_factory` | +| DUP-05 | `sdl3gpu_shader.cpp:18-35` + `crtpi_pipeline.cpp:17-34` | vertex shader MSL duplicado | Menor | PENDING | compartir string | +| DUP-06 | `surface.cpp:14-43` | constructor Surface abre y parsea GIF 2 veces | Importante | PENDING | `loadGif` devuelve pixels+palette | +| DUP-07 | `crtpi_pipeline.hpp:70-87` + `shader_backend.hpp:30-45` + `crtpi_pipeline.cpp:45-62` | defaults CRT-Pi triples | Importante | PENDING | single source of truth | + +## 2. Hardcoding + +| ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | +|----|---------------|----------|-----------|--------|-------| +| HC-01 | `text.cpp:159` | `render(1, 15)` — `1` (tinta) y `15` (color por defecto) literales | Importante | PENDING | `INK_INDEX` / `DEFAULT_COLOR` | +| HC-02 | `surface.cpp:29`, `screen.cpp:56,218,244` | `0xFF000000U`/`0xFF` alfa repetido | Menor | PENDING | `OPAQUE_ALPHA` | +| HC-03 | `gif.cpp:71,95,…` | LZW `<11`/`<12`/`<2` y máscaras `0x80/0x70/0x07…` sin nombre | Menor | PENDING | constantes con nombre | +| HC-04 | `notifier.hpp:41-46`, `render_info.hpp:38-40` | HUD constants hardcoded en clase, no en `defaults`/YAML | Importante | PENDING | `Defaults::Hud::…` o config.yaml | +| HC-05 | `render_info.cpp:19` + `notifier.hpp:43` | `TEXT_SIZE=8` duplicado | Menor | PENDING | constante global font | +| HC-06 | `render_info.cpp:64,138-139,193-195` | `segments_[3]` hardcode del segmento tiempo | Importante | PENDING | `enum SegmentId` | +| HC-07 | `render_info.cpp:207` | `% 3` hardcode nº de entradas enum | Menor | PENDING | `RENDER_INFO_POSITION_COUNT` | +| HC-08 | `crtpi_pipeline.cpp:97` | `768.0f / h / 3.0f` magic en shader | Menor | PENDING | uniforme o comentario | +| HC-09 | `render_info.cpp:179-183,194-195` | shadow offset +1,+1 magic | Menor | PENDING | `SHADOW_DIST` | +| HC-10 | `gif.cpp:162` | `processImageDescriptor` params `[[maybe_unused]]` vestigiales | Menor | PENDING | quitar args | + +## 3. Parches e improvisaciones + +| ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | +|----|---------------|----------|-----------|--------|-------| +| PATCH-01 | `screen.cpp:71-92,210-214`, `sdl3gpu_shader.cpp:445-493` | workarounds Wayland/Vulkan | Importante | WONTFIX (de momento) | RAII para forzar orden — deferred | +| PATCH-02 | `render_info.cpp:85-88` | guard bug-fix animation | Menor | PENDING | refactor FSM animación | +| PATCH-03 | `notifier.cpp:78-85` | TODO implícito de cola FIFO | Menor | WONTFIX | documentar decisión | +| PATCH-04 | `sprite.cpp:47-49` | `Sprite::update` vacío hook no invocado | Menor | PENDING | borrar (ver DEAD-10) | +| PATCH-05 | `gif.cpp:12-15` | comentario "reemplaçar macro READ" | Menor | PENDING | limpiar comentario | +| PATCH-06 | `gif.cpp:83-86` | guard self-reference defensivo | Menor | WONTFIX | dejar, cambiar `cerr` → logger futuro | +| PATCH-07 | `sdl3gpu_shader.cpp:17,162` + `crtpi_pipeline.cpp:16,162` | `NOLINTBEGIN` gigantes sobre MSL | Importante | PENDING | se resuelve con CONV-03 | +| PATCH-08 | `render_info.cpp:64,138-139` | `mono_digits=true` asignado 2 veces | Menor | PENDING | init única | + +## 4. Acoplamiento innecesario + +| ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | +|----|---------------|----------|-----------|--------|-------| +| COUP-01 | `notifier.hpp:9`, `render_info.cpp:12-13` | `core/rendering` incluye `game/ui/*` y `game/options.hpp` | Crítico | PENDING | inyectar via Config/callback | +| COUP-02 | `screen.cpp:13-14,109` | `Screen` instancia `SDL3GPUShader` concreto | Importante | PENDING | factory/registry | +| COUP-03 | `surface.cpp:81-83,110-112` | `Surface::render` usa `Screen::get()` global | Importante | PENDING | pasar destino por parámetro | +| COUP-04 | `notifier.cpp:19`, `render_info.cpp:52` | `Resource::Cache::get().getSurface("8bithud.gif")` con string literal | Importante | PENDING | inyectar la surface | +| COUP-05 | `animated_sprite.cpp:9,95-116` | parser YAML incrustado en sprite | Importante | PENDING | `AnimationLoader` en `core/resources` | +| COUP-06 | `notifier.hpp:22`, `render_info.hpp:17`, `screen.hpp:13` | 3 Meyers singletons en mismo subsistema | Importante | DEFERRED | requiere tocar Director | + +## 5. Inconsistencias internas + +| ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | +|----|---------------|----------|-----------|--------|-------| +| INC-01 | todo el subsistema | smart pointers mezclados (shared/unique/raw sin política) | Importante | DEFERRED | requiere decisión de política | +| INC-02 | `screen.cpp:52,104,125` vs `gif.cpp:96` vs `sdl3gpu_shader.cpp:214` | `exit`/throw/`bool` mezclados | Importante | DEFERRED | decisión global | +| INC-03 | `screen.cpp`, `sdl3gpu_shader.cpp`, `surface.cpp`, `gif.cpp` | `fprintf`/`cerr`/`cout` mezclados | Menor | DEFERRED | logger inexistente | +| INC-04 | `text.hpp:48-57` | API discreta + `writeDX(flags)` coexisten | Importante | PENDING | elegir una | +| INC-05 | `moving_sprite.cpp:39-40,58-72` | `pos_.x = static_cast(x_)` sobre `SDL_FRect` | Importante | PENDING | fijar semántica | +| INC-06 | `moving_sprite.cpp:12-21` | `Sprite::pos_ = pos;` redundante tras ctor delegado | Menor | PENDING | limpiar | +| INC-07 | `sprite.cpp:19-22` | initializer posicional vs designated | Menor | PENDING | uniformar | + +## 6. Código muerto + +| ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | +|----|---------------|----------|-----------|--------|-------| +| DEAD-01 | `text.hpp:50-51` | `writeShadowed`, `writeCentered` sin callers externos | Importante | PENDING | borrar | +| DEAD-02 | `text.hpp:62-63,67` | `glyphWidth`, `getGlyphClip`, `codepointToUtf8` sin callers | Importante | PENDING | borrar | +| DEAD-03 | `text.hpp:64` | `getSprite()` sin callers | Menor | PENDING | borrar | +| DEAD-04 | `sprite.hpp:33` | `getRect()` sin callers | Menor | PENDING | borrar | +| DEAD-05 | `sprite.hpp:42-43` | `incX`, `incY` sin callers | Menor | PENDING | borrar | +| DEAD-06 | `sprite.hpp:12`, `moving_sprite.hpp:13` | ctors default sin callers | Menor | PENDING | borrar | +| DEAD-07 | `animated_sprite.hpp:30` | `getCurrentAnimationSize()` sin callers | Menor | PENDING | borrar | +| DEAD-08 | `surface.hpp:61` | `getSurfaceData()` sin callers externos | Menor | PENDING | borrar o privatizar | +| DEAD-09 | `gif.hpp:45-65` | `Extension`, `ApplicationExtension`, `PlaintextExtension`, `GraphicControlExtension` no instanciados | Menor | PENDING | borrar | +| DEAD-10 | `sprite.cpp:47-49` | `Sprite::update` hook no invocado | Menor | PENDING | borrar (cruza PATCH-04) | + +## 7. Violaciones de convenciones del proyecto + +| ID | Hallazgo | Cruza con | Estado | +|----|----------|-----------|--------| +| CONV-01 | Magic numbers vs "sin magic numbers" | HC-* | cubierto por HC | +| CONV-02 | HUD hardcoded vs "configurabilidad desde ficheros" | HC-04 | cubierto por HC-04 | +| CONV-03 | Shaders MSL como raw strings en `.cpp` | DUP-04/05, PATCH-07 | PENDING | +| CONV-04 | Singletons vs "interfaces + callbacks" | COUP-06 | DEFERRED | +| CONV-05 | `core → game` deps | COUP-01 | cubierto por COUP-01 | + +--- + +## Registro de commits + +(Se irá rellenando a medida que se apliquen los cambios.) From 9434e4e87d4321b007557070772e779bde780254 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:06:40 +0200 Subject: [PATCH 02/19] refactor(core/rendering): desacoplar Notifier de game/ui (COUP-01a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core/rendering/notifier.hpp ja no inclou game/ui/notification_presets.hpp, i Notifier només exposa l'API crua notify(text, text_color, bg_color, time_ms). El helper de conveniència basat en Preset es trasllada com a funció lliure Notification::notify() a game/ui/notification_presets.hpp (direcció game → core, correcta). Els callers de global_inputs.cpp passen a usar el helper i afegeixen l'include explícit. Sota l'auditoria de core/rendering (docs/audit-core-rendering.md). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 8 +++++--- source/core/input/global_inputs.cpp | 13 +++++++------ source/core/rendering/notifier.cpp | 8 -------- source/core/rendering/notifier.hpp | 12 +++--------- source/game/ui/notification_presets.hpp | 15 ++++++++++++--- 5 files changed, 27 insertions(+), 29 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 5fc1286..9cc648a 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -18,7 +18,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Título | Estado | |----|--------|--------| -| COUP-01 | Inversión de dependencia `core/rendering → game/*` | PENDING | +| COUP-01 | Inversión de dependencia `core/rendering → game/*` | WIP (a: DONE, b: PENDING) | | DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | PENDING | | CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | PENDING | | DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | PENDING | @@ -70,7 +70,8 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| -| COUP-01 | `notifier.hpp:9`, `render_info.cpp:12-13` | `core/rendering` incluye `game/ui/*` y `game/options.hpp` | Crítico | PENDING | inyectar via Config/callback | +| COUP-01a | `notifier.hpp:9` | `core/rendering/notifier.hpp` incluye `game/ui/notification_presets.hpp` | Crítico | DONE | helper `Notification::notify()` en `game/ui` | +| COUP-01b | `render_info.cpp:12-13` | `core/rendering/render_info.cpp` incluye `game/options.hpp` + `game/ui/ui_palette.hpp` | Crítico | PENDING | `RenderInfo::State` inyectada pel Director | | COUP-02 | `screen.cpp:13-14,109` | `Screen` instancia `SDL3GPUShader` concreto | Importante | PENDING | factory/registry | | COUP-03 | `surface.cpp:81-83,110-112` | `Surface::render` usa `Screen::get()` global | Importante | PENDING | pasar destino por parámetro | | COUP-04 | `notifier.cpp:19`, `render_info.cpp:52` | `Resource::Cache::get().getSurface("8bithud.gif")` con string literal | Importante | PENDING | inyectar la surface | @@ -118,4 +119,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic ## Registro de commits -(Se irá rellenando a medida que se apliquen los cambios.) +- `9243db1` — docs: full de treball inicial (aquest fitxer) +- `` — COUP-01a: `Notifier` desacoblat de `game/ui/notification_presets` diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index 7bf6d9a..0714807 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -12,6 +12,7 @@ #include "core/rendering/screen.hpp" #include "core/system/event_buffer.hpp" #include "game/options.hpp" +#include "game/ui/notification_presets.hpp" #include "utils/defaults.hpp" namespace GlobalInputs { @@ -37,7 +38,7 @@ namespace GlobalInputs { } void notifyLocale(const std::string& key) { - Notifier::get().notify(Locale::get().text(key)); + Notification::notify(Locale::get().text(key)); } void onDecZoom() { @@ -46,7 +47,7 @@ namespace GlobalInputs { 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)); + Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom)); } void onIncZoom() { @@ -55,7 +56,7 @@ namespace GlobalInputs { 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)); + Notification::notify(Locale::get().text("input.zoom") + " x" + std::to_string(Options::window.zoom)); } void onFullscreen() { @@ -83,7 +84,7 @@ namespace GlobalInputs { 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); + Notification::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 @@ -108,7 +109,7 @@ namespace GlobalInputs { 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); + Notification::notify(Locale::get().text("input.preset") + ": " + name); } void onNextScalingMode() { @@ -151,7 +152,7 @@ namespace GlobalInputs { return; } exit_window_s = EXIT_WINDOW_S; - Notifier::get().notify(Locale::get().text("input.press_again_exit"), Notification::EXIT); + Notification::notify(Locale::get().text("input.press_again_exit"), Notification::EXIT); } } // namespace diff --git a/source/core/rendering/notifier.cpp b/source/core/rendering/notifier.cpp index 2524677..1c59600 100644 --- a/source/core/rendering/notifier.cpp +++ b/source/core/rendering/notifier.cpp @@ -67,14 +67,6 @@ void Notifier::render() { current_text_color_); } -void Notifier::notify(const std::string& text) { - notify(text, Notification::DEFAULT); -} - -void Notifier::notify(const std::string& text, const Notification::Preset& preset) { - notify(text, preset.text_color, preset.bg_color, preset.time_ms); -} - void Notifier::notify(const std::string& text, Uint8 text_color, Uint8 bg_color, int time_ms) { // Política simple: la nova notificació reemplaça la que hi havia. Per // gestionar-ne una cua, canviar ací per una FIFO quan calga. diff --git a/source/core/rendering/notifier.hpp b/source/core/rendering/notifier.hpp index 7682635..0f4dc09 100644 --- a/source/core/rendering/notifier.hpp +++ b/source/core/rendering/notifier.hpp @@ -6,8 +6,6 @@ #include #include -#include "game/ui/notification_presets.hpp" - class Text; // Popup transitori a dalt i centrat. Entra lliscant des de dalt, es queda @@ -15,10 +13,9 @@ class Text; // 8bithud i la paleta "ui" amb la resta d'overlays (carregades per RenderInfo // a l'arrencada; si Notifier::init() es crida primer, les carrega ell). // -// Ús típic: -// Notifier::get().notify("Text ràpid"); // preset DEFAULT -// Notifier::get().notify("Text ràpid", miPreset); // preset custom -// Notifier::get().notify("Urgent", 11, 3, 800); // colors i durada explícits +// API crua: Notifier::get().notify(text, text_color, bg_color, time_ms). +// Per a treballar amb presets del joc, vegeu Notification::notify() a +// game/ui/notification_presets.hpp. class Notifier { public: static auto get() -> Notifier&; @@ -33,9 +30,6 @@ class Notifier { void update(int delta_ms); void render(); - // Tres overloads; els dos simples deleguen al tercer. - void notify(const std::string& text); - void notify(const std::string& text, const Notification::Preset& preset); void notify(const std::string& text, Uint8 text_color, Uint8 bg_color, int time_ms); static constexpr int PADDING_H = 6; diff --git a/source/game/ui/notification_presets.hpp b/source/game/ui/notification_presets.hpp index d758964..c56639e 100644 --- a/source/game/ui/notification_presets.hpp +++ b/source/game/ui/notification_presets.hpp @@ -2,11 +2,14 @@ #include +#include + +#include "core/rendering/notifier.hpp" + // Temes per a les notificacions de pantalla. Un "preset" captura els dos // colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat -// en mil·lisegons. Qualsevol component que cride Notifier::notify(text) -// rebrà l'estil DEFAULT; si necessita un altre look, pot passar el preset per -// nom o especificar els camps un a un. +// en mil·lisegons. Per a llançar una notificació amb un preset concret, +// cridar Notification::notify(text, preset); si s'omet, s'usa DEFAULT. namespace Notification { struct Preset { @@ -34,4 +37,10 @@ namespace Notification { .time_ms = 4000, }; + // Helper de conveniència: aplica un preset (per defecte DEFAULT) cridant + // la API crua de Notifier. La direcció game → core és la correcta. + inline void notify(const std::string& text, const Preset& preset = DEFAULT) { + Notifier::get().notify(text, preset.text_color, preset.bg_color, preset.time_ms); + } + } // namespace Notification From 9395520a23358719ddb8efe62cd22d442ede2297 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:13:58 +0200 Subject: [PATCH 03/19] refactor(core/rendering): desacoplar RenderInfo d'Options i UiColor (COUP-01b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core/rendering/render_info ja no inclou game/options.hpp ni game/ui/ui_palette.hpp. S'introdueix RenderInfo::Frame amb tot l'estat extern (posició, show_time, shader_enabled, shader_name, shader_preset, colors) que el caller injecta a cada init()/update(). La lògica de persistència (cyclePosition/toggleShowTime) surt de RenderInfo i passa a global_inputs.cpp, que muta Options i crida saveToFile() directament. Options::buildRenderInfoFrame(text_color, shadow_color) centralitza la construcció del Frame seguint el mateix patró que buildScreenConfig / buildPostFXParams. Director i GameScene criden aquest builder quan necessiten un Frame (amb UiColor::RENDER_INFO_{TEXT,SHADOW} com a colors actius). Amb aquest canvi queda tancada COUP-01: core/rendering/ no té cap include de game/*. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 7 +-- source/core/input/global_inputs.cpp | 7 ++- source/core/rendering/render_info.cpp | 65 +++++++++++---------------- source/core/rendering/render_info.hpp | 32 ++++++++----- source/core/system/director.cpp | 3 +- source/game/options.cpp | 15 +++++++ source/game/options.hpp | 4 ++ source/game/scenes/game_scene.cpp | 3 +- 8 files changed, 77 insertions(+), 59 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 9cc648a..cea883b 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -18,7 +18,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Título | Estado | |----|--------|--------| -| COUP-01 | Inversión de dependencia `core/rendering → game/*` | WIP (a: DONE, b: PENDING) | +| COUP-01 | Inversión de dependencia `core/rendering → game/*` | DONE | | DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | PENDING | | CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | PENDING | | DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | PENDING | @@ -71,7 +71,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| | COUP-01a | `notifier.hpp:9` | `core/rendering/notifier.hpp` incluye `game/ui/notification_presets.hpp` | Crítico | DONE | helper `Notification::notify()` en `game/ui` | -| COUP-01b | `render_info.cpp:12-13` | `core/rendering/render_info.cpp` incluye `game/options.hpp` + `game/ui/ui_palette.hpp` | Crítico | PENDING | `RenderInfo::State` inyectada pel Director | +| COUP-01b | `render_info.cpp:12-13` | `core/rendering/render_info.cpp` incluye `game/options.hpp` + `game/ui/ui_palette.hpp` | Crítico | DONE | `RenderInfo::Frame` injectada; Options.buildRenderInfoFrame() | | COUP-02 | `screen.cpp:13-14,109` | `Screen` instancia `SDL3GPUShader` concreto | Importante | PENDING | factory/registry | | COUP-03 | `surface.cpp:81-83,110-112` | `Surface::render` usa `Screen::get()` global | Importante | PENDING | pasar destino por parámetro | | COUP-04 | `notifier.cpp:19`, `render_info.cpp:52` | `Resource::Cache::get().getSurface("8bithud.gif")` con string literal | Importante | PENDING | inyectar la surface | @@ -120,4 +120,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic ## Registro de commits - `9243db1` — docs: full de treball inicial (aquest fitxer) -- `` — COUP-01a: `Notifier` desacoblat de `game/ui/notification_presets` +- `9434e4e` — COUP-01a: `Notifier` desacoblat de `game/ui/notification_presets` +- `` — COUP-01b: `RenderInfo` desacoblat de `game/options` + `game/ui/ui_palette` diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index 0714807..a365b90 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -139,11 +139,14 @@ namespace GlobalInputs { } void onCycleRenderInfo() { - RenderInfo::cyclePosition(); + const int NEXT = (static_cast(Options::video.render_info_position) + 1) % 3; + Options::video.render_info_position = static_cast(NEXT); + Options::saveToFile(); } void onToggleRenderTime() { - RenderInfo::toggleShowTime(); + Options::video.render_info_show_time = !Options::video.render_info_show_time; + Options::saveToFile(); } void onExit() { diff --git a/source/core/rendering/render_info.cpp b/source/core/rendering/render_info.cpp index b9414b8..ab5ef75 100644 --- a/source/core/rendering/render_info.cpp +++ b/source/core/rendering/render_info.cpp @@ -9,8 +9,6 @@ #include "core/rendering/screen.hpp" #include "core/rendering/text.hpp" #include "core/resources/resource_cache.hpp" -#include "game/options.hpp" -#include "game/ui/ui_palette.hpp" #include "utils/defines.hpp" #include "utils/easing.hpp" @@ -23,6 +21,10 @@ namespace { // Evita una fileta d'un sol píxel quan acaba de començar a aparèixer. constexpr float VISIBLE_EPSILON = 0.01F; + // Índex del segment de temps dins de segments_. Si canvia SEGMENT_COUNT + // o l'ordre, actualitzar ací. + constexpr int TIME_SEGMENT = 3; + auto prettify(std::string s) -> std::string { std::ranges::replace(s, '-', ' '); std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); }); @@ -48,27 +50,25 @@ auto RenderInfo::get() -> RenderInfo& { RenderInfo::~RenderInfo() = default; -void RenderInfo::init() { +void RenderInfo::init(const Frame& initial) { auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); + current_ = initial; + segments_[TIME_SEGMENT].mono_digits = true; + // Restaura l'estat guardat: si l'overlay estava engegat, l'activem ja // plenament visible (sense animació d'entrada inicial). - if (Options::video.render_info_position != Defaults::RenderInfoPosition::OFF) { - visible_pos_ = Options::video.render_info_position; + if (initial.target_position != Defaults::RenderInfoPosition::OFF) { + visible_pos_ = initial.target_position; info_anim_ = 1.0F; - } - - // El segment de temps pot estar activat per persistència: si la barra - // apareix ja plena, el seu segment també ha d'estar-ho. - segments_[3].mono_digits = true; - if (info_anim_ >= 1.0F) { refreshSegments(); for (auto& seg : segments_) { seg.anim = seg.visible ? 1.0F : 0.0F; } } } -void RenderInfo::update(int delta_ms) { +void RenderInfo::update(int delta_ms, const Frame& frame) { + current_ = frame; elapsed_ms_ += delta_ms; const float DT = static_cast(delta_ms) / 1000.0F; @@ -76,7 +76,7 @@ void RenderInfo::update(int delta_ms) { // visible, entrem (anim→1). Si no, primer eixim (anim→0) del lloc actual; // quan arriba a 0 commutem visible_pos_ i deixem que el proper frame // dispari l'entrada. Excepció: de OFF a TOP/BOTTOM commutem en sec. - const auto DESIRED = Options::video.render_info_position; + const auto DESIRED = current_.target_position; if (DESIRED == visible_pos_) { // Si el destí és OFF, no cal animar cap a un lloc — la barra ja ha // d'estar fora. Sense aquest guard, en arribar de BOTTOM→OFF la @@ -122,21 +122,18 @@ void RenderInfo::refreshSegments() { segments_[1].visible = true; segments_[1].mono_digits = false; - // [2] Shader + preset (només amb shader activat) - if (Options::video.shader.enabled) { - const std::string& shader = Options::video.shader.current_shader; - const std::string& preset = (shader == "crtpi") - ? Options::video.shader.current_crtpi_preset - : Options::video.shader.current_postfx_preset; - segments_[2].text = " | " + shader + " " + prettify(preset); + // [2] Shader + preset (només amb shader activat). El caller ha triat el + // preset actiu segons el shader; ací només el formatem. + if (current_.shader_enabled) { + segments_[2].text = " | " + current_.shader_name + " " + prettify(current_.shader_preset); } - segments_[2].visible = Options::video.shader.enabled; + segments_[2].visible = current_.shader_enabled; segments_[2].mono_digits = false; // [3] Temps de joc (togglejable i persistent) - segments_[3].text = " | " + formatTime(elapsed_ms_); - segments_[3].visible = Options::video.render_info_show_time; - segments_[3].mono_digits = true; + segments_[TIME_SEGMENT].text = " | " + formatTime(elapsed_ms_); + segments_[TIME_SEGMENT].visible = current_.show_time; + segments_[TIME_SEGMENT].mono_digits = true; // Els tres primers segments baixem a minúscules per coherència estètica // amb la resta de la línia; el [3] (temps) manté dígits i puntuació. @@ -191,25 +188,13 @@ void RenderInfo::render() { const int SEG_W = segmentWidth(seg, DIGIT_CELL); if (seg.mono_digits) { - text_->writeMonoDigits(XI + 1, Y + 1, seg.text, UiColor::RENDER_INFO_SHADOW, DIGIT_CELL); - text_->writeMonoDigits(XI, Y, seg.text, UiColor::RENDER_INFO_TEXT, DIGIT_CELL); + text_->writeMonoDigits(XI + 1, Y + 1, seg.text, current_.shadow_color, DIGIT_CELL); + text_->writeMonoDigits(XI, Y, seg.text, current_.text_color, DIGIT_CELL); } else { - text_->writeColored(XI + 1, Y + 1, seg.text, UiColor::RENDER_INFO_SHADOW); - text_->writeColored(XI, Y, seg.text, UiColor::RENDER_INFO_TEXT); + text_->writeColored(XI + 1, Y + 1, seg.text, current_.shadow_color); + text_->writeColored(XI, Y, seg.text, current_.text_color); } cur_x += static_cast(SEG_W) * Easing::outQuad(seg.anim); } } - -void RenderInfo::cyclePosition() { - const auto CURRENT = Options::video.render_info_position; - const int NEXT = (static_cast(CURRENT) + 1) % 3; - Options::video.render_info_position = static_cast(NEXT); - Options::saveToFile(); -} - -void RenderInfo::toggleShowTime() { - Options::video.render_info_show_time = !Options::video.render_info_show_time; - Options::saveToFile(); -} diff --git a/source/core/rendering/render_info.hpp b/source/core/rendering/render_info.hpp index 193f5f3..21bbd6f 100644 --- a/source/core/rendering/render_info.hpp +++ b/source/core/rendering/render_info.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -11,9 +13,9 @@ class Text; // Overlay de debug amb tres estats (OFF / TOP / BOTTOM). Mostra una barra // d'info dividida en segments independents (fps, driver, shader, temps) que -// apareixen i desapareixen suaument. La posició es commuta amb una tecla -// (cicle OFF → TOP → BOTTOM) i el segment de temps es pot mostrar o amagar -// per separat; les dos opcions persisteixen a config.yaml. +// apareixen i desapareixen suaument. No llig cap configuració; el caller +// (Director a init, GameScene a update) injecta cada frame l'estat desitjat +// via Frame. class RenderInfo { public: static auto get() -> RenderInfo&; @@ -23,18 +25,23 @@ class RenderInfo { auto operator=(const RenderInfo&) -> RenderInfo& = delete; auto operator=(RenderInfo&&) -> RenderInfo& = delete; - void init(); + // Estat extern que dirigeix què ha de mostrar RenderInfo. La posició + // determina si la barra entra per dalt o per baix (o s'amaga). El + // shader_preset ja ha d'estar pre-seleccionat (crtpi/postfx) pel caller. + struct Frame { + Defaults::RenderInfoPosition target_position{Defaults::RenderInfoPosition::OFF}; + bool show_time{false}; + bool shader_enabled{false}; + std::string shader_name; + std::string shader_preset; + Uint8 text_color{0}; + Uint8 shadow_color{0}; + }; - void update(int delta_ms); + void init(const Frame& initial); + void update(int delta_ms, const Frame& frame); void render(); - // Avança la posició OFF → TOP → BOTTOM → OFF i persisteix. - static void cyclePosition(); - - // Mostra/oculta el segment de temps i persisteix. Només té efecte visual - // quan la barra és visible (TOP/BOTTOM). - static void toggleShowTime(); - static constexpr int HEIGHT = 10; static constexpr float SLIDE_SPEED = 5.0F; // posició global (1/s) static constexpr float SEG_SPEED = 6.0F; // segments individuals (1/s) @@ -60,4 +67,5 @@ class RenderInfo { float info_anim_{0.0F}; // 0..1, suavitzat amb outQuad al render std::array segments_{}; int elapsed_ms_{0}; + Frame current_{}; }; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 3a4ef2a..a18d25b 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -22,6 +22,7 @@ #include "core/system/event_buffer.hpp" #include "game/options.hpp" #include "game/scenes/game_scene.hpp" +#include "game/ui/ui_palette.hpp" #include "utils/defaults.hpp" Director::Director() { @@ -79,7 +80,7 @@ Director::Director() { // JA_Music_t que continga, així que no queden streams orfes. Audio::init(Options::buildAudioConfig()); - RenderInfo::get().init(); + RenderInfo::get().init(Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); Notifier::get().init(); current_scene_ = std::make_unique(); diff --git a/source/game/options.cpp b/source/game/options.cpp index bac7813..cea9241 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -553,6 +553,21 @@ namespace Options { return Rendering::ShaderType::POSTFX; } + auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame { + const std::string& active_preset = (video.shader.current_shader == "crtpi") + ? video.shader.current_crtpi_preset + : video.shader.current_postfx_preset; + return { + .target_position = video.render_info_position, + .show_time = video.render_info_show_time, + .shader_enabled = video.shader.enabled, + .shader_name = video.shader.current_shader, + .shader_preset = active_preset, + .text_color = text_color, + .shadow_color = shadow_color, + }; + } + auto buildCrtPiParams() -> Rendering::CrtPiParams { // Si el shader està deshabilitat, retornem defaults (el dispatch anirà per // PostFX passthrough, però sent nets per si algú consulta aquests params). diff --git a/source/game/options.hpp b/source/game/options.hpp index f754d75..b6e9d05 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -4,6 +4,7 @@ #include #include "core/audio/audio.hpp" +#include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/shader_backend.hpp" #include "utils/defaults.hpp" @@ -121,6 +122,9 @@ namespace Options { [[nodiscard]] auto buildPostFXParams() -> Rendering::PostFXParams; [[nodiscard]] auto buildCrtPiParams() -> Rendering::CrtPiParams; [[nodiscard]] auto buildActiveShader() -> Rendering::ShaderType; + // Els colors de text/ombra els decideix la UI del joc (no Options), per + // això es passen com a paràmetres. + [[nodiscard]] auto buildRenderInfoFrame(Uint8 text_color, Uint8 shadow_color) -> RenderInfo::Frame; // `::Audio` és la classe global d'àudio (no `Options::Audio`, que és la // struct serialitzable). Retornem la seua Config per passar-la a init(). [[nodiscard]] auto buildAudioConfig() -> ::Audio::Config; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index e48c9da..615e061 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -15,6 +15,7 @@ #include "core/resources/resource_cache.hpp" #include "game/entities/player.hpp" #include "game/map/map.hpp" +#include "game/options.hpp" #include "game/ui/ui_palette.hpp" #include "utils/defaults.hpp" #include "utils/defines.hpp" @@ -60,7 +61,7 @@ void GameScene::update(int delta_ms) { const float DT = static_cast(delta_ms) / 1000.0F; for (auto& player : players_) { player->update(DT); } - RenderInfo::get().update(delta_ms); + RenderInfo::get().update(delta_ms, Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); Notifier::get().update(delta_ms); } From d479475dcf015cf02a8e6f6bfc8abecac38564c8 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:19:09 +0200 Subject: [PATCH 04/19] =?UTF-8?q?refactor(core/rendering):=20deduplicar=20?= =?UTF-8?q?Surface::render=20via=20delegaci=C3=B3=20(DUP-02)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface::render i Surface::renderWithColorReplace compartien ~30 línies del bucle de blit (clipping, flip, transparent check, escriptura ARGB) i només diferien en l'ARGB final. Ara render() delega a la versió amb remap passant source==target==transparent_color_: qualsevol píxel opac té COLOR != source_color (els transparents ja s'han filtrat), per tant REMAPPED == sub_palette_[COLOR] — resultat idèntic al path original. Verificat amb make build + smoke test de 5s sota xvfb-run (exit 124 = timeout sencer, sense crash). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 7 ++++--- source/core/rendering/surface.cpp | 31 +++++-------------------------- 2 files changed, 9 insertions(+), 29 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index cea883b..da23023 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -19,7 +19,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Título | Estado | |----|--------|--------| | COUP-01 | Inversión de dependencia `core/rendering → game/*` | DONE | -| DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | PENDING | +| DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | DONE | | CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | PENDING | | DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | PENDING | | COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | PENDING | @@ -31,7 +31,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| | DUP-01 | `gif.cpp:238-276` | 3 ramas del switch extension saltan bloque+subloops idénticos | Importante | PENDING | helper `skipExtensionBlocks` | -| DUP-02 | `surface.cpp:80-137` | `render` vs `renderWithColorReplace` ~90% idénticos | Crítico | PENDING | inner loop compartido | +| DUP-02 | `surface.cpp:80-137` | `render` vs `renderWithColorReplace` ~90% idénticos | Crítico | DONE | `render()` delega amb source==target==transparent | | DUP-03 | `text.cpp:145-289` | bucle UTF-8 repetido en write/writeColored/writeMonoDigits/length/lengthMonoDigits | Importante | PENDING | iterador de glifos | | DUP-04 | `sdl3gpu_shader.cpp:165-201` + `crtpi_pipeline.cpp:165-202` | `createShaderMSL/SPIRV` duplicados | Importante | PENDING | extraer a `shader_factory` | | DUP-05 | `sdl3gpu_shader.cpp:18-35` + `crtpi_pipeline.cpp:17-34` | vertex shader MSL duplicado | Menor | PENDING | compartir string | @@ -121,4 +121,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `9243db1` — docs: full de treball inicial (aquest fitxer) - `9434e4e` — COUP-01a: `Notifier` desacoblat de `game/ui/notification_presets` -- `` — COUP-01b: `RenderInfo` desacoblat de `game/options` + `game/ui/ui_palette` +- `9395520` — COUP-01b: `RenderInfo` desacoblat de `game/options` + `game/ui/ui_palette` +- `` — DUP-02: `Surface::render` delega a `renderWithColorReplace` diff --git a/source/core/rendering/surface.cpp b/source/core/rendering/surface.cpp index d3ecd48..f41203b 100644 --- a/source/core/rendering/surface.cpp +++ b/source/core/rendering/surface.cpp @@ -78,32 +78,11 @@ auto Surface::getPixel(int x, int y) const -> Uint8 { } void Surface::render(int x, int y, const SDL_FRect* src_rect, SDL_FlipMode flip) const { - Uint32* dst = Screen::get().getCanvasArgb(); - const int DST_W = Screen::get().getCanvasWidth(); - const int DST_H = Screen::get().getCanvasHeight(); - - const int SX = (src_rect != nullptr) ? static_cast(src_rect->x) : 0; - const int SY = (src_rect != nullptr) ? static_cast(src_rect->y) : 0; - int sw = (src_rect != nullptr) ? static_cast(src_rect->w) : surface_data_->width; - int sh = (src_rect != nullptr) ? static_cast(src_rect->h) : surface_data_->height; - - sw = std::min(sw, surface_data_->width - SX); - sh = std::min(sh, surface_data_->height - SY); - - const Uint8* src = surface_data_->data.get(); - for (int iy = 0; iy < sh; ++iy) { - for (int ix = 0; ix < sw; ++ix) { - const int SRC_X = (flip == SDL_FLIP_HORIZONTAL) ? (SX + sw - 1 - ix) : (SX + ix); - const int SRC_Y = (flip == SDL_FLIP_VERTICAL) ? (SY + sh - 1 - iy) : (SY + iy); - const int DST_X = x + ix; - const int DST_Y = y + iy; - if (DST_X < 0 || DST_X >= DST_W || DST_Y < 0 || DST_Y >= DST_H) { continue; } - - const Uint8 COLOR = src[SRC_X + (SRC_Y * surface_data_->width)]; - if (COLOR == transparent_color_) { continue; } - dst[DST_X + (DST_Y * DST_W)] = palette_[sub_palette_[COLOR]]; - } - } + // Delegem a la versió amb remap: fent source==target==transparent_color_, + // qualsevol píxel opaC tindrà COLOR != source_color (els transparents ja + // s'han filtrat), així que REMAPPED = sub_palette_[COLOR] — idèntic al + // camí sense remap. Això evita duplicar el bucle de blit. + renderWithColorReplace(x, y, transparent_color_, transparent_color_, src_rect, flip); } void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, const SDL_FRect* src_rect, SDL_FlipMode flip) const { From 5f46fb22a4c464722b3208ca6be01c5206f53730 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:28:52 +0200 Subject: [PATCH 05/19] refactor(sdl3gpu): extreure shaders MSL i helpers a headers compartits (CONV-03) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Els dos pipelines GPU (PostFX i CrtPi) tenien: - Raw strings MSL incrustades al .cpp amb NOLINTBEGIN(readability- identifier-naming) envoltant ~150 línies cadascun. - Funcions createShaderMSL / createShaderSPIRV duplicades byte a byte en cada .cpp (DUP-04), amb un comentari admetent-ho. - El vertex shader postfx_vs escrit dues voltes, idèntic (DUP-05). S'introdueixen: - source/core/rendering/sdl3gpu/shader_factory.hpp amb els dos helpers inline, per a reutilitzar des de qualsevol pipeline. - source/core/rendering/sdl3gpu/msl/ amb un header per shader (postfx_vert compartit, postfx_frag, crtpi_frag); cada constant es diu kCamelCase i cau per defecte sota el project naming, així que els blocs NOLINT desapareixen (PATCH-07). - Suppresió a cppcheck per a sdl3gpu/msl/* (és MSL, no C++). Els .cpp es redueixen ~190 línies cada un i queden només amb la lògica del pipeline. Build + smoke test de 5s sota xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 1 + docs/audit-core-rendering.md | 13 +- .../rendering/sdl3gpu/msl/crtpi_frag.msl.h | 138 ++++++++++++ .../rendering/sdl3gpu/msl/postfx_frag.msl.h | 137 ++++++++++++ .../rendering/sdl3gpu/msl/postfx_vert.msl.h | 30 +++ .../sdl3gpu/pipelines/crtpi_pipeline.cpp | 203 +----------------- .../core/rendering/sdl3gpu/sdl3gpu_shader.cpp | 200 +---------------- .../core/rendering/sdl3gpu/shader_factory.hpp | 50 +++++ 8 files changed, 377 insertions(+), 395 deletions(-) create mode 100644 source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h create mode 100644 source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h create mode 100644 source/core/rendering/sdl3gpu/msl/postfx_vert.msl.h create mode 100644 source/core/rendering/sdl3gpu/shader_factory.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 234e381..51cee2a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,7 @@ if(CPPCHECK_EXE) --suppress=toomanyconfigs --suppress=*:*source/external/* --suppress=*:*source/core/rendering/sdl3gpu/spv/* + --suppress=*:*source/core/rendering/sdl3gpu/msl/* --suppress=normalCheckLevelMaxBranches -D_DEBUG -DLINUX_BUILD diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index da23023..bea2f77 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -20,7 +20,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic |----|--------|--------| | COUP-01 | Inversión de dependencia `core/rendering → game/*` | DONE | | DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | DONE | -| CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | PENDING | +| CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | DONE | | DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | PENDING | | COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | PENDING | @@ -33,8 +33,8 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | DUP-01 | `gif.cpp:238-276` | 3 ramas del switch extension saltan bloque+subloops idénticos | Importante | PENDING | helper `skipExtensionBlocks` | | DUP-02 | `surface.cpp:80-137` | `render` vs `renderWithColorReplace` ~90% idénticos | Crítico | DONE | `render()` delega amb source==target==transparent | | DUP-03 | `text.cpp:145-289` | bucle UTF-8 repetido en write/writeColored/writeMonoDigits/length/lengthMonoDigits | Importante | PENDING | iterador de glifos | -| DUP-04 | `sdl3gpu_shader.cpp:165-201` + `crtpi_pipeline.cpp:165-202` | `createShaderMSL/SPIRV` duplicados | Importante | PENDING | extraer a `shader_factory` | -| DUP-05 | `sdl3gpu_shader.cpp:18-35` + `crtpi_pipeline.cpp:17-34` | vertex shader MSL duplicado | Menor | PENDING | compartir string | +| DUP-04 | `sdl3gpu_shader.cpp:165-201` + `crtpi_pipeline.cpp:165-202` | `createShaderMSL/SPIRV` duplicados | Importante | DONE | `sdl3gpu/shader_factory.hpp` | +| DUP-05 | `sdl3gpu_shader.cpp:18-35` + `crtpi_pipeline.cpp:17-34` | vertex shader MSL duplicado | Menor | DONE | `sdl3gpu/msl/postfx_vert.msl.h` compartit | | DUP-06 | `surface.cpp:14-43` | constructor Surface abre y parsea GIF 2 veces | Importante | PENDING | `loadGif` devuelve pixels+palette | | DUP-07 | `crtpi_pipeline.hpp:70-87` + `shader_backend.hpp:30-45` + `crtpi_pipeline.cpp:45-62` | defaults CRT-Pi triples | Importante | PENDING | single source of truth | @@ -63,7 +63,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | PATCH-04 | `sprite.cpp:47-49` | `Sprite::update` vacío hook no invocado | Menor | PENDING | borrar (ver DEAD-10) | | PATCH-05 | `gif.cpp:12-15` | comentario "reemplaçar macro READ" | Menor | PENDING | limpiar comentario | | PATCH-06 | `gif.cpp:83-86` | guard self-reference defensivo | Menor | WONTFIX | dejar, cambiar `cerr` → logger futuro | -| PATCH-07 | `sdl3gpu_shader.cpp:17,162` + `crtpi_pipeline.cpp:16,162` | `NOLINTBEGIN` gigantes sobre MSL | Importante | PENDING | se resuelve con CONV-03 | +| PATCH-07 | `sdl3gpu_shader.cpp:17,162` + `crtpi_pipeline.cpp:16,162` | `NOLINTBEGIN` gigantes sobre MSL | Importante | DONE | eliminats amb CONV-03 | | PATCH-08 | `render_info.cpp:64,138-139` | `mono_digits=true` asignado 2 veces | Menor | PENDING | init única | ## 4. Acoplamiento innecesario @@ -111,7 +111,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic |----|----------|-----------|--------| | CONV-01 | Magic numbers vs "sin magic numbers" | HC-* | cubierto por HC | | CONV-02 | HUD hardcoded vs "configurabilidad desde ficheros" | HC-04 | cubierto por HC-04 | -| CONV-03 | Shaders MSL como raw strings en `.cpp` | DUP-04/05, PATCH-07 | PENDING | +| CONV-03 | Shaders MSL como raw strings en `.cpp` | DUP-04/05, PATCH-07 | DONE | | CONV-04 | Singletons vs "interfaces + callbacks" | COUP-06 | DEFERRED | | CONV-05 | `core → game` deps | COUP-01 | cubierto por COUP-01 | @@ -122,4 +122,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `9243db1` — docs: full de treball inicial (aquest fitxer) - `9434e4e` — COUP-01a: `Notifier` desacoblat de `game/ui/notification_presets` - `9395520` — COUP-01b: `RenderInfo` desacoblat de `game/options` + `game/ui/ui_palette` -- `` — DUP-02: `Surface::render` delega a `renderWithColorReplace` +- `d479475` — DUP-02: `Surface::render` delega a `renderWithColorReplace` +- `` — CONV-03 + DUP-04 + DUP-05 + PATCH-07: shaders MSL i helpers extrets a `sdl3gpu/msl/` i `sdl3gpu/shader_factory.hpp` diff --git a/source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h b/source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h new file mode 100644 index 0000000..e770feb --- /dev/null +++ b/source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h @@ -0,0 +1,138 @@ +#pragma once + +#ifdef __APPLE__ + +// Fragment shader del shader "crtpi" (algoritme CRT-Pi): scanlines amb +// pesos gaussians, multisample opcional, gamma i màscara de subpíxels. +namespace Rendering::Msl { + + inline constexpr const char* kCrtpiFrag = R"( +#include +using namespace metal; + +struct PostVOut { + float4 pos [[position]]; + float2 uv; +}; + +struct CrtPiUniforms { + float scanline_weight; + float scanline_gap_brightness; + float bloom_factor; + float input_gamma; + float output_gamma; + float mask_brightness; + float curvature_x; + float curvature_y; + int mask_type; + int enable_scanlines; + int enable_multisample; + int enable_gamma; + int enable_curvature; + int enable_sharper; + float texture_width; + float texture_height; +}; + +static float2 crtpi_distort(float2 coord, float2 screen_scale, float cx, float cy) { + float2 curvature = float2(cx, cy); + float2 barrel_scale = 1.0f - (0.23f * curvature); + coord *= screen_scale; + coord -= 0.5f; + float rsq = coord.x * coord.x + coord.y * coord.y; + coord += coord * (curvature * rsq); + coord *= barrel_scale; + if (abs(coord.x) >= 0.5f || abs(coord.y) >= 0.5f) { return float2(-1.0f); } + coord += 0.5f; + coord /= screen_scale; + return coord; +} + +static float crtpi_scan_weight(float dist, float sw, float gap) { + return max(1.0f - dist * dist * sw, gap); +} + +static float crtpi_scan_line(float dy, float filter_w, float sw, float gap, bool ms) { + float w = crtpi_scan_weight(dy, sw, gap); + if (ms) { + w += crtpi_scan_weight(dy - filter_w, sw, gap); + w += crtpi_scan_weight(dy + filter_w, sw, gap); + w *= 0.3333333f; + } + return w; +} + +fragment float4 crtpi_fs(PostVOut in [[stage_in]], + texture2d tex [[texture(0)]], + sampler samp [[sampler(0)]], + constant CrtPiUniforms& u [[buffer(0)]]) { + float2 tex_size = float2(u.texture_width, u.texture_height); + float filter_width = (768.0f / u.texture_height) / 3.0f; + float2 texcoord = in.uv; + + if (u.enable_curvature != 0) { + texcoord = crtpi_distort(texcoord, float2(1.0f, 1.0f), u.curvature_x, u.curvature_y); + if (texcoord.x < 0.0f) { return float4(0.0f, 0.0f, 0.0f, 1.0f); } + } + + float2 coord_in_pixels = texcoord * tex_size; + float2 tc; + float scan_weight; + + if (u.enable_sharper != 0) { + float2 temp = floor(coord_in_pixels) + 0.5f; + tc = temp / tex_size; + float2 deltas = coord_in_pixels - temp; + scan_weight = crtpi_scan_line(deltas.y, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0); + float2 signs = sign(deltas); + deltas.x *= 2.0f; + deltas = deltas * deltas; + deltas.y = deltas.y * deltas.y; + deltas.x *= 0.5f; + deltas.y *= 8.0f; + deltas /= tex_size; + deltas *= signs; + tc = tc + deltas; + } else { + float temp_y = floor(coord_in_pixels.y) + 0.5f; + float y_coord = temp_y / tex_size.y; + float dy = coord_in_pixels.y - temp_y; + scan_weight = crtpi_scan_line(dy, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0); + float sign_y = sign(dy); + dy = dy * dy; + dy = dy * dy; + dy *= 8.0f; + dy /= tex_size.y; + dy *= sign_y; + tc = float2(texcoord.x, y_coord + dy); + } + + float3 colour = tex.sample(samp, tc).rgb; + + if (u.enable_scanlines != 0) { + if (u.enable_gamma != 0) { colour = pow(colour, float3(u.input_gamma)); } + colour *= scan_weight * u.bloom_factor; + if (u.enable_gamma != 0) { colour = pow(colour, float3(1.0f / u.output_gamma)); } + } + + if (u.mask_type == 1) { + float wm = fract(in.pos.x * 0.5f); + float3 mask = (wm < 0.5f) ? float3(u.mask_brightness, 1.0f, u.mask_brightness) + : float3(1.0f, u.mask_brightness, 1.0f); + colour *= mask; + } else if (u.mask_type == 2) { + float wm = fract(in.pos.x * 0.3333333f); + float3 mask = float3(u.mask_brightness); + if (wm < 0.3333333f) mask.x = 1.0f; + else if (wm < 0.6666666f) mask.y = 1.0f; + else mask.z = 1.0f; + colour *= mask; + } + + return float4(colour, 1.0f); +} +)"; + +} // namespace Rendering::Msl + +#endif // __APPLE__ diff --git a/source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h b/source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h new file mode 100644 index 0000000..0a9fce8 --- /dev/null +++ b/source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h @@ -0,0 +1,137 @@ +#pragma once + +#ifdef __APPLE__ + +// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask, +// gamma, curvature, bleeding i flicker. Els paràmetres venen via uniforms. +namespace Rendering::Msl { + + inline constexpr const char* kPostfxFrag = R"( +#include +using namespace metal; + +struct PostVOut { + float4 pos [[position]]; + float2 uv; +}; + +struct PostFXUniforms { + float vignette_strength; + float chroma_strength; + float scanline_strength; + float screen_height; + float mask_strength; + float gamma_strength; + float curvature; + float bleeding; + float pixel_scale; + float time; + float oversample; + float flicker; +}; + +static float3 rgb_to_ycc(float3 rgb) { + return float3( + 0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b, + -0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f, + 0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f + ); +} +static float3 ycc_to_rgb(float3 ycc) { + float y = ycc.x; + float cb = ycc.y - 0.5f; + float cr = ycc.z - 0.5f; + return clamp(float3( + y + 1.402f*cr, + y - 0.344f*cb - 0.714f*cr, + y + 1.772f*cb + ), 0.0f, 1.0f); +} + +fragment float4 postfx_fs(PostVOut in [[stage_in]], + texture2d scene [[texture(0)]], + sampler samp [[sampler(0)]], + constant PostFXUniforms& u [[buffer(0)]]) { + float2 uv = in.uv; + + if (u.curvature > 0.0f) { + float2 c = uv - 0.5f; + float rsq = dot(c, c); + float2 dist = float2(0.05f, 0.1f) * u.curvature; + float2 barrelScale = 1.0f - 0.23f * dist; + c += c * (dist * rsq); + c *= barrelScale; + if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) { + return float4(0.0f, 0.0f, 0.0f, 1.0f); + } + uv = c + 0.5f; + } + + float3 base = scene.sample(samp, uv).rgb; + + float3 colour; + if (u.bleeding > 0.0f) { + float tw = float(scene.get_width()); + float step = u.oversample / tw; + float3 ycc = rgb_to_ycc(base); + float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb); + float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb); + float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb); + float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb); + ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f; + colour = mix(base, ycc_to_rgb(ycc), u.bleeding); + } else { + colour = base; + } + + float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f)); + colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r; + colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b; + + if (u.gamma_strength > 0.0f) { + float3 lin = pow(colour, float3(2.4f)); + colour = mix(colour, lin, u.gamma_strength); + } + + const float SCAN_DARK_RATIO = 0.333f; + const float SCAN_DARK_FLOOR = 0.42f; + if (u.scanline_strength > 0.0f) { + float ps = max(1.0f, round(u.pixel_scale)); + float frac_in_row = fract(uv.y * u.screen_height); + float row_pos = floor(frac_in_row * ps); + float bright_rows = (ps < 2.0f) ? ps : ((ps < 3.0f) ? 1.0f : floor(ps * (1.0f - SCAN_DARK_RATIO))); + float is_dark = step(bright_rows, row_pos); + float scan = mix(1.0f, SCAN_DARK_FLOOR, is_dark); + colour *= mix(1.0f, scan, u.scanline_strength); + } + + if (u.gamma_strength > 0.0f) { + float3 enc = pow(colour, float3(1.0f/2.2f)); + colour = mix(colour, enc, u.gamma_strength); + } + + float2 d = uv - 0.5f; + float vignette = 1.0f - dot(d, d) * u.vignette_strength; + colour *= clamp(vignette, 0.0f, 1.0f); + + if (u.mask_strength > 0.0f) { + float whichMask = fract(in.pos.x * 0.3333333f); + float3 mask = float3(0.80f); + if (whichMask < 0.3333333f) mask.x = 1.0f; + else if (whichMask < 0.6666667f) mask.y = 1.0f; + else mask.z = 1.0f; + colour = mix(colour, colour * mask, u.mask_strength); + } + + if (u.flicker > 0.0f) { + float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f; + colour *= 1.0f - u.flicker * 0.04f * flicker_wave; + } + + return float4(colour, 1.0f); +} +)"; + +} // namespace Rendering::Msl + +#endif // __APPLE__ diff --git a/source/core/rendering/sdl3gpu/msl/postfx_vert.msl.h b/source/core/rendering/sdl3gpu/msl/postfx_vert.msl.h new file mode 100644 index 0000000..dc50b38 --- /dev/null +++ b/source/core/rendering/sdl3gpu/msl/postfx_vert.msl.h @@ -0,0 +1,30 @@ +#pragma once + +#ifdef __APPLE__ + +// Vertex shader compartit per tots els pipelines de post-procés: +// fullscreen-triangle que cobreix tota l'àrea del swapchain amb UVs a [0,1]. +namespace Rendering::Msl { + + inline constexpr const char* kPostfxVert = R"( +#include +using namespace metal; + +struct PostVOut { + float4 pos [[position]]; + float2 uv; +}; + +vertex PostVOut postfx_vs(uint vid [[vertex_id]]) { + const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} }; + const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} }; + PostVOut out; + out.pos = float4(positions[vid], 0.0, 1.0); + out.uv = uvs[vid]; + return out; +} +)"; + +} // namespace Rendering::Msl + +#endif // __APPLE__ diff --git a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp index 63e1114..9aed88b 100644 --- a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp +++ b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp @@ -3,206 +3,17 @@ #include #include +#include "core/rendering/sdl3gpu/shader_factory.hpp" #include "core/rendering/sdl3gpu/viewport.hpp" -#ifndef __APPLE__ +#ifdef __APPLE__ +#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h" +#include "core/rendering/sdl3gpu/msl/postfx_vert.msl.h" +#else #include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h" #endif -namespace { - -#ifdef __APPLE__ - // NOLINTBEGIN(readability-identifier-naming) - const char* CRTPI_VERT_MSL_ = R"( -#include -using namespace metal; - -struct PostVOut { - float4 pos [[position]]; - float2 uv; -}; - -vertex PostVOut postfx_vs(uint vid [[vertex_id]]) { - const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} }; - const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} }; - PostVOut out; - out.pos = float4(positions[vid], 0.0, 1.0); - out.uv = uvs[vid]; - return out; -} -)"; - - const char* CRTPI_FRAG_MSL_ = R"( -#include -using namespace metal; - -struct PostVOut { - float4 pos [[position]]; - float2 uv; -}; - -struct CrtPiUniforms { - float scanline_weight; - float scanline_gap_brightness; - float bloom_factor; - float input_gamma; - float output_gamma; - float mask_brightness; - float curvature_x; - float curvature_y; - int mask_type; - int enable_scanlines; - int enable_multisample; - int enable_gamma; - int enable_curvature; - int enable_sharper; - float texture_width; - float texture_height; -}; - -static float2 crtpi_distort(float2 coord, float2 screen_scale, float cx, float cy) { - float2 curvature = float2(cx, cy); - float2 barrel_scale = 1.0f - (0.23f * curvature); - coord *= screen_scale; - coord -= 0.5f; - float rsq = coord.x * coord.x + coord.y * coord.y; - coord += coord * (curvature * rsq); - coord *= barrel_scale; - if (abs(coord.x) >= 0.5f || abs(coord.y) >= 0.5f) { return float2(-1.0f); } - coord += 0.5f; - coord /= screen_scale; - return coord; -} - -static float crtpi_scan_weight(float dist, float sw, float gap) { - return max(1.0f - dist * dist * sw, gap); -} - -static float crtpi_scan_line(float dy, float filter_w, float sw, float gap, bool ms) { - float w = crtpi_scan_weight(dy, sw, gap); - if (ms) { - w += crtpi_scan_weight(dy - filter_w, sw, gap); - w += crtpi_scan_weight(dy + filter_w, sw, gap); - w *= 0.3333333f; - } - return w; -} - -fragment float4 crtpi_fs(PostVOut in [[stage_in]], - texture2d tex [[texture(0)]], - sampler samp [[sampler(0)]], - constant CrtPiUniforms& u [[buffer(0)]]) { - float2 tex_size = float2(u.texture_width, u.texture_height); - float filter_width = (768.0f / u.texture_height) / 3.0f; - float2 texcoord = in.uv; - - if (u.enable_curvature != 0) { - texcoord = crtpi_distort(texcoord, float2(1.0f, 1.0f), u.curvature_x, u.curvature_y); - if (texcoord.x < 0.0f) { return float4(0.0f, 0.0f, 0.0f, 1.0f); } - } - - float2 coord_in_pixels = texcoord * tex_size; - float2 tc; - float scan_weight; - - if (u.enable_sharper != 0) { - float2 temp = floor(coord_in_pixels) + 0.5f; - tc = temp / tex_size; - float2 deltas = coord_in_pixels - temp; - scan_weight = crtpi_scan_line(deltas.y, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0); - float2 signs = sign(deltas); - deltas.x *= 2.0f; - deltas = deltas * deltas; - deltas.y = deltas.y * deltas.y; - deltas.x *= 0.5f; - deltas.y *= 8.0f; - deltas /= tex_size; - deltas *= signs; - tc = tc + deltas; - } else { - float temp_y = floor(coord_in_pixels.y) + 0.5f; - float y_coord = temp_y / tex_size.y; - float dy = coord_in_pixels.y - temp_y; - scan_weight = crtpi_scan_line(dy, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0); - float sign_y = sign(dy); - dy = dy * dy; - dy = dy * dy; - dy *= 8.0f; - dy /= tex_size.y; - dy *= sign_y; - tc = float2(texcoord.x, y_coord + dy); - } - - float3 colour = tex.sample(samp, tc).rgb; - - if (u.enable_scanlines != 0) { - if (u.enable_gamma != 0) { colour = pow(colour, float3(u.input_gamma)); } - colour *= scan_weight * u.bloom_factor; - if (u.enable_gamma != 0) { colour = pow(colour, float3(1.0f / u.output_gamma)); } - } - - if (u.mask_type == 1) { - float wm = fract(in.pos.x * 0.5f); - float3 mask = (wm < 0.5f) ? float3(u.mask_brightness, 1.0f, u.mask_brightness) - : float3(1.0f, u.mask_brightness, 1.0f); - colour *= mask; - } else if (u.mask_type == 2) { - float wm = fract(in.pos.x * 0.3333333f); - float3 mask = float3(u.mask_brightness); - if (wm < 0.3333333f) mask.x = 1.0f; - else if (wm < 0.6666666f) mask.y = 1.0f; - else mask.z = 1.0f; - colour *= mask; - } - - return float4(colour, 1.0f); -} -)"; - // NOLINTEND(readability-identifier-naming) -#endif - - // Helpers per a crear shaders. Duplicats en cada pipeline per mantindre els - // fitxers autònoms — són funcions trivials i no val la pena un header compartit. -#ifdef __APPLE__ - auto createShaderMSL(SDL_GPUDevice* device, - const char* msl_source, - const char* entrypoint, - SDL_GPUShaderStage stage, - Uint32 num_samplers, - Uint32 num_uniform_buffers) -> SDL_GPUShader* { - SDL_GPUShaderCreateInfo info{}; - info.code = reinterpret_cast(msl_source); - info.code_size = std::strlen(msl_source) + 1; - info.entrypoint = entrypoint; - info.format = SDL_GPU_SHADERFORMAT_MSL; - info.stage = stage; - info.num_samplers = num_samplers; - info.num_uniform_buffers = num_uniform_buffers; - return SDL_CreateGPUShader(device, &info); - } -#else - auto createShaderSPIRV(SDL_GPUDevice* device, - const std::uint8_t* spv_code, - std::size_t spv_size, - const char* entrypoint, - SDL_GPUShaderStage stage, - Uint32 num_samplers, - Uint32 num_uniform_buffers) -> SDL_GPUShader* { - SDL_GPUShaderCreateInfo info{}; - info.code = spv_code; - info.code_size = spv_size; - info.entrypoint = entrypoint; - info.format = SDL_GPU_SHADERFORMAT_SPIRV; - info.stage = stage; - info.num_samplers = num_samplers; - info.num_uniform_buffers = num_uniform_buffers; - return SDL_CreateGPUShader(device, &info); - } -#endif - -} // namespace - namespace Rendering { CrtPiPipeline::~CrtPiPipeline() { cleanup(); } @@ -211,8 +22,8 @@ namespace Rendering { device_ = device; #ifdef __APPLE__ - SDL_GPUShader* vert = createShaderMSL(device_, CRTPI_VERT_MSL_, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - SDL_GPUShader* frag = createShaderMSL(device_, CRTPI_FRAG_MSL_, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + SDL_GPUShader* vert = createShaderMSL(device_, Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); + SDL_GPUShader* frag = createShaderMSL(device_, Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); #else SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index 56ee556..be40cb6 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -4,203 +4,17 @@ #include #include +#include "core/rendering/sdl3gpu/shader_factory.hpp" #include "core/rendering/sdl3gpu/viewport.hpp" -#ifndef __APPLE__ +#ifdef __APPLE__ +#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h" +#include "core/rendering/sdl3gpu/msl/postfx_vert.msl.h" +#else #include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h" #endif -namespace { - -#ifdef __APPLE__ - // NOLINTBEGIN(readability-identifier-naming) - const char* POSTFX_VERT_MSL_ = R"( -#include -using namespace metal; - -struct PostVOut { - float4 pos [[position]]; - float2 uv; -}; - -vertex PostVOut postfx_vs(uint vid [[vertex_id]]) { - const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} }; - const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} }; - PostVOut out; - out.pos = float4(positions[vid], 0.0, 1.0); - out.uv = uvs[vid]; - return out; -} -)"; - - const char* POSTFX_FRAG_MSL_ = R"( -#include -using namespace metal; - -struct PostVOut { - float4 pos [[position]]; - float2 uv; -}; - -struct PostFXUniforms { - float vignette_strength; - float chroma_strength; - float scanline_strength; - float screen_height; - float mask_strength; - float gamma_strength; - float curvature; - float bleeding; - float pixel_scale; - float time; - float oversample; - float flicker; -}; - -static float3 rgb_to_ycc(float3 rgb) { - return float3( - 0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b, - -0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f, - 0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f - ); -} -static float3 ycc_to_rgb(float3 ycc) { - float y = ycc.x; - float cb = ycc.y - 0.5f; - float cr = ycc.z - 0.5f; - return clamp(float3( - y + 1.402f*cr, - y - 0.344f*cb - 0.714f*cr, - y + 1.772f*cb - ), 0.0f, 1.0f); -} - -fragment float4 postfx_fs(PostVOut in [[stage_in]], - texture2d scene [[texture(0)]], - sampler samp [[sampler(0)]], - constant PostFXUniforms& u [[buffer(0)]]) { - float2 uv = in.uv; - - if (u.curvature > 0.0f) { - float2 c = uv - 0.5f; - float rsq = dot(c, c); - float2 dist = float2(0.05f, 0.1f) * u.curvature; - float2 barrelScale = 1.0f - 0.23f * dist; - c += c * (dist * rsq); - c *= barrelScale; - if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) { - return float4(0.0f, 0.0f, 0.0f, 1.0f); - } - uv = c + 0.5f; - } - - float3 base = scene.sample(samp, uv).rgb; - - float3 colour; - if (u.bleeding > 0.0f) { - float tw = float(scene.get_width()); - float step = u.oversample / tw; - float3 ycc = rgb_to_ycc(base); - float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb); - float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb); - float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb); - float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb); - ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f; - colour = mix(base, ycc_to_rgb(ycc), u.bleeding); - } else { - colour = base; - } - - float ca = u.chroma_strength * 0.005f * (1.0f + 0.15f * sin(u.time * 7.3f)); - colour.r = scene.sample(samp, uv + float2(ca, 0.0f)).r; - colour.b = scene.sample(samp, uv - float2(ca, 0.0f)).b; - - if (u.gamma_strength > 0.0f) { - float3 lin = pow(colour, float3(2.4f)); - colour = mix(colour, lin, u.gamma_strength); - } - - const float SCAN_DARK_RATIO = 0.333f; - const float SCAN_DARK_FLOOR = 0.42f; - if (u.scanline_strength > 0.0f) { - float ps = max(1.0f, round(u.pixel_scale)); - float frac_in_row = fract(uv.y * u.screen_height); - float row_pos = floor(frac_in_row * ps); - float bright_rows = (ps < 2.0f) ? ps : ((ps < 3.0f) ? 1.0f : floor(ps * (1.0f - SCAN_DARK_RATIO))); - float is_dark = step(bright_rows, row_pos); - float scan = mix(1.0f, SCAN_DARK_FLOOR, is_dark); - colour *= mix(1.0f, scan, u.scanline_strength); - } - - if (u.gamma_strength > 0.0f) { - float3 enc = pow(colour, float3(1.0f/2.2f)); - colour = mix(colour, enc, u.gamma_strength); - } - - float2 d = uv - 0.5f; - float vignette = 1.0f - dot(d, d) * u.vignette_strength; - colour *= clamp(vignette, 0.0f, 1.0f); - - if (u.mask_strength > 0.0f) { - float whichMask = fract(in.pos.x * 0.3333333f); - float3 mask = float3(0.80f); - if (whichMask < 0.3333333f) mask.x = 1.0f; - else if (whichMask < 0.6666667f) mask.y = 1.0f; - else mask.z = 1.0f; - colour = mix(colour, colour * mask, u.mask_strength); - } - - if (u.flicker > 0.0f) { - float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f; - colour *= 1.0f - u.flicker * 0.04f * flicker_wave; - } - - return float4(colour, 1.0f); -} -)"; - // NOLINTEND(readability-identifier-naming) -#endif - -#ifdef __APPLE__ - auto createShaderMSL(SDL_GPUDevice* device, - const char* msl_source, - const char* entrypoint, - SDL_GPUShaderStage stage, - std::uint32_t num_samplers, - std::uint32_t num_uniform_buffers) -> SDL_GPUShader* { - SDL_GPUShaderCreateInfo info{}; - info.code = reinterpret_cast(msl_source); - info.code_size = std::strlen(msl_source) + 1; - info.entrypoint = entrypoint; - info.format = SDL_GPU_SHADERFORMAT_MSL; - info.stage = stage; - info.num_samplers = num_samplers; - info.num_uniform_buffers = num_uniform_buffers; - return SDL_CreateGPUShader(device, &info); - } -#else - auto createShaderSPIRV(SDL_GPUDevice* device, - const std::uint8_t* spv_code, - std::size_t spv_size, - const char* entrypoint, - SDL_GPUShaderStage stage, - std::uint32_t num_samplers, - std::uint32_t num_uniform_buffers) -> SDL_GPUShader* { - SDL_GPUShaderCreateInfo info{}; - info.code = spv_code; - info.code_size = spv_size; - info.entrypoint = entrypoint; - info.format = SDL_GPU_SHADERFORMAT_SPIRV; - info.stage = stage; - info.num_samplers = num_samplers; - info.num_uniform_buffers = num_uniform_buffers; - return SDL_CreateGPUShader(device, &info); - } -#endif - -} // namespace - namespace Rendering { SDL3GPUShader::~SDL3GPUShader() { cleanup(); } @@ -261,8 +75,8 @@ namespace Rendering { auto SDL3GPUShader::createShaders() -> bool { #ifdef __APPLE__ - vertex_shader_ = createShaderMSL(device_, POSTFX_VERT_MSL_, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); - fragment_shader_ = createShaderMSL(device_, POSTFX_FRAG_MSL_, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); + vertex_shader_ = createShaderMSL(device_, Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); + fragment_shader_ = createShaderMSL(device_, Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); #else vertex_shader_ = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); diff --git a/source/core/rendering/sdl3gpu/shader_factory.hpp b/source/core/rendering/sdl3gpu/shader_factory.hpp new file mode 100644 index 0000000..4154c04 --- /dev/null +++ b/source/core/rendering/sdl3gpu/shader_factory.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include + +// Helpers de creació de shaders SDL3 GPU. Inline per evitar un .cpp +// addicional per a funcions trivials. +namespace Rendering { + +#ifdef __APPLE__ + inline auto createShaderMSL(SDL_GPUDevice* device, + const char* msl_source, + const char* entrypoint, + SDL_GPUShaderStage stage, + Uint32 num_samplers, + Uint32 num_uniform_buffers) -> SDL_GPUShader* { + SDL_GPUShaderCreateInfo info{}; + info.code = reinterpret_cast(msl_source); + info.code_size = std::strlen(msl_source) + 1; + info.entrypoint = entrypoint; + info.format = SDL_GPU_SHADERFORMAT_MSL; + info.stage = stage; + info.num_samplers = num_samplers; + info.num_uniform_buffers = num_uniform_buffers; + return SDL_CreateGPUShader(device, &info); + } +#else + inline auto createShaderSPIRV(SDL_GPUDevice* device, + const std::uint8_t* spv_code, + std::size_t spv_size, + const char* entrypoint, + SDL_GPUShaderStage stage, + Uint32 num_samplers, + Uint32 num_uniform_buffers) -> SDL_GPUShader* { + SDL_GPUShaderCreateInfo info{}; + info.code = spv_code; + info.code_size = spv_size; + info.entrypoint = entrypoint; + info.format = SDL_GPU_SHADERFORMAT_SPIRV; + info.stage = stage; + info.num_samplers = num_samplers; + info.num_uniform_buffers = num_uniform_buffers; + return SDL_CreateGPUShader(device, &info); + } +#endif + +} // namespace Rendering From a63deac6a3a3eaeb076c426db4137b35dd0cbb1c Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:32:37 +0200 Subject: [PATCH 06/19] refactor(core/rendering): deduplicar Text::write* i nomenar magic numbers (DUP-03 + HC-01) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text tenia cinc mètodes (write, writeColored, writeMonoDigits, length, lengthMonoDigits) i dos getters (glyphWidth, getGlyphClip) que repetien el mateix patró de resolució de codepoint: find(cp), fallback a '?' si no hi és. Extret a un helper privat resolveGlyph(cp) que centralitza aquesta lògica; totes les funcions passen a usar-lo. write() ara delega directament a writeColored(...) passant el color per defecte, eliminant ~19 línies del bucle UTF-8 duplicades. Els literals 1 (índex de "tinta" al PNG) i 15 (color per defecte a la paleta) passen a ser Text::INK_COLOR_INDEX i Text::DEFAULT_TEXT_COLOR (HC-01). Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 9 ++-- source/core/rendering/text.cpp | 80 +++++++++++++--------------------- source/core/rendering/text.hpp | 11 +++++ 3 files changed, 47 insertions(+), 53 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index bea2f77..684e399 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -21,7 +21,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | COUP-01 | Inversión de dependencia `core/rendering → game/*` | DONE | | DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | DONE | | CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | DONE | -| DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | PENDING | +| DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | DONE | | COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | PENDING | --- @@ -32,7 +32,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic |----|---------------|----------|-----------|--------|-------| | DUP-01 | `gif.cpp:238-276` | 3 ramas del switch extension saltan bloque+subloops idénticos | Importante | PENDING | helper `skipExtensionBlocks` | | DUP-02 | `surface.cpp:80-137` | `render` vs `renderWithColorReplace` ~90% idénticos | Crítico | DONE | `render()` delega amb source==target==transparent | -| DUP-03 | `text.cpp:145-289` | bucle UTF-8 repetido en write/writeColored/writeMonoDigits/length/lengthMonoDigits | Importante | PENDING | iterador de glifos | +| DUP-03 | `text.cpp:145-289` | bucle UTF-8 repetido en write/writeColored/writeMonoDigits/length/lengthMonoDigits | Importante | DONE | `resolveGlyph` + `write` delega a `writeColored` | | DUP-04 | `sdl3gpu_shader.cpp:165-201` + `crtpi_pipeline.cpp:165-202` | `createShaderMSL/SPIRV` duplicados | Importante | DONE | `sdl3gpu/shader_factory.hpp` | | DUP-05 | `sdl3gpu_shader.cpp:18-35` + `crtpi_pipeline.cpp:17-34` | vertex shader MSL duplicado | Menor | DONE | `sdl3gpu/msl/postfx_vert.msl.h` compartit | | DUP-06 | `surface.cpp:14-43` | constructor Surface abre y parsea GIF 2 veces | Importante | PENDING | `loadGif` devuelve pixels+palette | @@ -42,7 +42,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| -| HC-01 | `text.cpp:159` | `render(1, 15)` — `1` (tinta) y `15` (color por defecto) literales | Importante | PENDING | `INK_INDEX` / `DEFAULT_COLOR` | +| HC-01 | `text.cpp:159` | `render(1, 15)` — `1` (tinta) y `15` (color por defecto) literales | Importante | DONE | `Text::INK_COLOR_INDEX` i `DEFAULT_TEXT_COLOR` | | HC-02 | `surface.cpp:29`, `screen.cpp:56,218,244` | `0xFF000000U`/`0xFF` alfa repetido | Menor | PENDING | `OPAQUE_ALPHA` | | HC-03 | `gif.cpp:71,95,…` | LZW `<11`/`<12`/`<2` y máscaras `0x80/0x70/0x07…` sin nombre | Menor | PENDING | constantes con nombre | | HC-04 | `notifier.hpp:41-46`, `render_info.hpp:38-40` | HUD constants hardcoded en clase, no en `defaults`/YAML | Importante | PENDING | `Defaults::Hud::…` o config.yaml | @@ -123,4 +123,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `9434e4e` — COUP-01a: `Notifier` desacoblat de `game/ui/notification_presets` - `9395520` — COUP-01b: `RenderInfo` desacoblat de `game/options` + `game/ui/ui_palette` - `d479475` — DUP-02: `Surface::render` delega a `renderWithColorReplace` -- `` — CONV-03 + DUP-04 + DUP-05 + PATCH-07: shaders MSL i helpers extrets a `sdl3gpu/msl/` i `sdl3gpu/shader_factory.hpp` +- `5f46fb2` — CONV-03 + DUP-04 + DUP-05 + PATCH-07: shaders MSL i helpers extrets a `sdl3gpu/msl/` i `sdl3gpu/shader_factory.hpp` +- `` — DUP-03 + HC-01: `Text` amb `resolveGlyph` i constants `INK_COLOR_INDEX` / `DEFAULT_TEXT_COLOR` diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index 04c1ed1..1f40c73 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -140,27 +140,10 @@ Text::Text(const std::shared_ptr& surface, const std::shared_ptr& offset_(text_file->offset) { } -// Dibuixa la cadena amb la tinta per defecte (codi 15 a la paleta del png). -// Els glifs amb codepoint no registrat es substitueixen pel glif '?'. +// Dibuixa la cadena amb la tinta per defecte. Els glifs amb codepoint no +// registrat es substitueixen pel glif '?'. void Text::write(int x, int y, const std::string& text, int kerning, int length) { - int shift = 0; - int glyphs_done = 0; - std::size_t pos = 0; - - sprite_->setY(static_cast(y)); - while (pos < text.size()) { - if (length != -1 && glyphs_done >= length) { break; } - std::uint32_t cp = nextCodepoint(text, pos); - auto it = offset_.find(cp); - if (it == offset_.end()) { it = offset_.find('?'); } - if (it != offset_.end()) { - sprite_->setClip(static_cast(it->second.x), static_cast(it->second.y), static_cast(box_width_), static_cast(box_height_)); - sprite_->setX(static_cast(x + shift)); - sprite_->render(1, 15); - shift += it->second.w + kerning; - } - ++glyphs_done; - } + writeColored(x, y, text, DEFAULT_TEXT_COLOR, kerning, length); } void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int length) { @@ -172,13 +155,11 @@ void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int while (pos < text.size()) { if (length != -1 && glyphs_done >= length) { break; } std::uint32_t cp = nextCodepoint(text, pos); - auto it = offset_.find(cp); - if (it == offset_.end()) { it = offset_.find('?'); } - if (it != offset_.end()) { - sprite_->setClip(static_cast(it->second.x), static_cast(it->second.y), static_cast(box_width_), static_cast(box_height_)); + if (const Offset* off = resolveGlyph(cp); off != nullptr) { + sprite_->setClip(static_cast(off->x), static_cast(off->y), static_cast(box_width_), static_cast(box_height_)); sprite_->setX(static_cast(x + shift)); - sprite_->render(1, color); - shift += it->second.w + kerning; + sprite_->render(INK_COLOR_INDEX, color); + shift += off->w + kerning; } ++glyphs_done; } @@ -235,21 +216,20 @@ void Text::writeMonoDigits(int x, int y, const std::string& text, Uint8 color, i sprite_->setY(static_cast(y)); while (pos < text.size()) { std::uint32_t cp = nextCodepoint(text, pos); - auto it = offset_.find(cp); - if (it == offset_.end()) { it = offset_.find('?'); } - if (it == offset_.end()) { continue; } + const Offset* off = resolveGlyph(cp); + if (off == nullptr) { continue; } const bool IS_DIGIT = (cp >= '0' && cp <= '9'); - const int GLYPH_W = it->second.w; + const int GLYPH_W = off->w; const int CELL_W = IS_DIGIT ? digit_cell_w : GLYPH_W; const int GLYPH_X = IS_DIGIT ? x + shift + ((CELL_W - GLYPH_W) / 2) : x + shift; - sprite_->setClip(static_cast(it->second.x), - static_cast(it->second.y), + sprite_->setClip(static_cast(off->x), + static_cast(off->y), static_cast(box_width_), static_cast(box_height_)); sprite_->setX(static_cast(GLYPH_X)); - sprite_->render(1, color); + sprite_->render(INK_COLOR_INDEX, color); shift += CELL_W + kerning; } @@ -261,12 +241,11 @@ auto Text::lengthMonoDigits(const std::string& text, int digit_cell_w, int kerni while (pos < text.size()) { std::uint32_t cp = nextCodepoint(text, pos); - auto it = offset_.find(cp); - if (it == offset_.end()) { it = offset_.find('?'); } - if (it == offset_.end()) { continue; } + const Offset* off = resolveGlyph(cp); + if (off == nullptr) { continue; } const bool IS_DIGIT = (cp >= '0' && cp <= '9'); - shift += (IS_DIGIT ? digit_cell_w : it->second.w) + kerning; + shift += (IS_DIGIT ? digit_cell_w : off->w) + kerning; } return shift > 0 ? shift - kerning : 0; @@ -278,10 +257,8 @@ auto Text::length(const std::string& text, int kerning) const -> int { while (pos < text.size()) { std::uint32_t cp = nextCodepoint(text, pos); - auto it = offset_.find(cp); - if (it == offset_.end()) { it = offset_.find('?'); } - if (it != offset_.end()) { - shift += it->second.w + kerning; + if (const Offset* off = resolveGlyph(cp); off != nullptr) { + shift += off->w + kerning; } } @@ -289,18 +266,23 @@ auto Text::length(const std::string& text, int kerning) const -> int { } auto Text::glyphWidth(std::uint32_t codepoint, int kerning) const -> int { - auto it = offset_.find(codepoint); - if (it == offset_.end()) { it = offset_.find('?'); } - if (it != offset_.end()) { return it->second.w + kerning; } + if (const Offset* off = resolveGlyph(codepoint); off != nullptr) { + return off->w + kerning; + } return 0; } auto Text::getGlyphClip(std::uint32_t codepoint) const -> SDL_FRect { - auto it = offset_.find(codepoint); - if (it == offset_.end()) { it = offset_.find('?'); } - if (it == offset_.end()) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; } - return {.x = static_cast(it->second.x), - .y = static_cast(it->second.y), + const Offset* off = resolveGlyph(codepoint); + if (off == nullptr) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; } + return {.x = static_cast(off->x), + .y = static_cast(off->y), .w = static_cast(box_width_), .h = static_cast(box_height_)}; } + +auto Text::resolveGlyph(std::uint32_t cp) const -> const Offset* { + auto it = offset_.find(cp); + if (it == offset_.end()) { it = offset_.find('?'); } + return (it != offset_.end()) ? &it->second : nullptr; +} diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp index f0e68fb..a65c023 100644 --- a/source/core/rendering/text.hpp +++ b/source/core/rendering/text.hpp @@ -36,6 +36,13 @@ class Text { static constexpr Uint8 CENTER_FLAG = 4; static constexpr Uint8 STROKE_FLAG = 8; + // Índex del píxel "tinta" al PNG de la font. Els glifs són monocroms + // sobre transparent i tots els píxels opacs usen aquest índex; la Surface + // el recolora al vol per a emetre el color destí. + static constexpr Uint8 INK_COLOR_INDEX = 1; + // Color per defecte a la paleta quan s'usa write() sense especificar-lo. + static constexpr Uint8 DEFAULT_TEXT_COLOR = 15; + Text(const std::shared_ptr& surface, const std::string& text_file); Text(const std::shared_ptr& surface, const std::shared_ptr& text_file); ~Text() = default; @@ -68,6 +75,10 @@ class Text { static auto nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t; private: + // Busca l'Offset del codepoint; si no hi és, cau al glif '?' com a + // substitut. Retorna nullptr si tampoc hi ha '?'. + [[nodiscard]] auto resolveGlyph(std::uint32_t cp) const -> const Offset*; + std::unique_ptr sprite_; int box_width_{0}; int box_height_{0}; From 029907d5a02a8e0d98c0008a7f63b0577939be5e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:36:11 +0200 Subject: [PATCH 07/19] refactor(core/rendering): Screen desacoblat de SDL3GPUShader concret (COUP-02a) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screen ja no inclou sdl3gpu/sdl3gpu_shader.hpp ni instancia la classe concreta. S'afegeix la fàbrica Rendering::makeDefaultShaderBackend() declarada a shader_backend.hpp i implementada a sdl3gpu_shader.cpp (únic fitxer que encara coneix la classe concreta). Per a que Screen no necessite static_cast, releaseWindow() passa a ser mètode virtual pur de la interfície ShaderBackend (abans només existia a SDL3GPUShader). SDL3GPUShader::releaseWindow marca override. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 7 ++++--- source/core/rendering/screen.cpp | 5 ++--- source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp | 4 ++++ source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp | 2 +- source/core/rendering/shader_backend.hpp | 10 ++++++++++ 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 684e399..45eefed 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -22,7 +22,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | DONE | | CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | DONE | | DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | DONE | -| COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | PENDING | +| COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | WIP (a: DONE, b: PENDING) | --- @@ -72,7 +72,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic |----|---------------|----------|-----------|--------|-------| | COUP-01a | `notifier.hpp:9` | `core/rendering/notifier.hpp` incluye `game/ui/notification_presets.hpp` | Crítico | DONE | helper `Notification::notify()` en `game/ui` | | COUP-01b | `render_info.cpp:12-13` | `core/rendering/render_info.cpp` incluye `game/options.hpp` + `game/ui/ui_palette.hpp` | Crítico | DONE | `RenderInfo::Frame` injectada; Options.buildRenderInfoFrame() | -| COUP-02 | `screen.cpp:13-14,109` | `Screen` instancia `SDL3GPUShader` concreto | Importante | PENDING | factory/registry | +| COUP-02a | `screen.cpp:13-14,109` | `Screen` instancia `SDL3GPUShader` concreto | Importante | DONE | `Rendering::makeDefaultShaderBackend()` factory | | COUP-03 | `surface.cpp:81-83,110-112` | `Surface::render` usa `Screen::get()` global | Importante | PENDING | pasar destino por parámetro | | COUP-04 | `notifier.cpp:19`, `render_info.cpp:52` | `Resource::Cache::get().getSurface("8bithud.gif")` con string literal | Importante | PENDING | inyectar la surface | | COUP-05 | `animated_sprite.cpp:9,95-116` | parser YAML incrustado en sprite | Importante | PENDING | `AnimationLoader` en `core/resources` | @@ -124,4 +124,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `9395520` — COUP-01b: `RenderInfo` desacoblat de `game/options` + `game/ui/ui_palette` - `d479475` — DUP-02: `Surface::render` delega a `renderWithColorReplace` - `5f46fb2` — CONV-03 + DUP-04 + DUP-05 + PATCH-07: shaders MSL i helpers extrets a `sdl3gpu/msl/` i `sdl3gpu/shader_factory.hpp` -- `` — DUP-03 + HC-01: `Text` amb `resolveGlyph` i constants `INK_COLOR_INDEX` / `DEFAULT_TEXT_COLOR` +- `a63deac` — DUP-03 + HC-01: `Text` amb `resolveGlyph` i constants `INK_COLOR_INDEX` / `DEFAULT_TEXT_COLOR` +- `` — COUP-02a: `Screen` desacoblat de `SDL3GPUShader` via `makeDefaultShaderBackend()` diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 82c7c56..6969554 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -10,7 +10,6 @@ #include #include -#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" #include "utils/defines.hpp" namespace { @@ -74,7 +73,7 @@ void Screen::destroySdl() { // 2) Destruir el GPU device (vkDestroyInstance amb Wayland encara viu). // 3) Destruir la finestra SDL (tanca la surface de Wayland al final). if (shader_backend_ != nullptr) { - static_cast(shader_backend_.get())->releaseWindow(); + shader_backend_->releaseWindow(); } shader_backend_.reset(); if (game_texture_ != nullptr) { @@ -106,7 +105,7 @@ void Screen::createSdl() { // Primer intent: path SDL3 GPU. Si falla, fallback automàtic a SDL_Renderer. if (config_.gpu_acceleration) { - auto backend = std::make_unique(); + auto backend = Rendering::makeDefaultShaderBackend(); backend->setPreferredDriver(config_.preferred_driver); backend->setVSync(config_.vsync); if (backend->init(window_, canvas_width_, canvas_height_)) { diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index be40cb6..f358a80 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -307,4 +307,8 @@ namespace Rendering { window_ = nullptr; } + auto makeDefaultShaderBackend() -> std::unique_ptr { + return std::make_unique(); + } + } // namespace Rendering diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index d1b0fca..f3931cf 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -60,7 +60,7 @@ namespace Rendering { // Allibera la finestra del device sense destruir el device. Cal cridar-ho // abans de SDL_DestroyWindow perquè el VkSurface tancat no sobrevisca la // finestra de Wayland (vkDestroyInstance cridaria Wayland amb surface invàlida). - void releaseWindow(); + void releaseWindow() override; [[nodiscard]] auto isReady() const -> bool override { return is_ready_; } [[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; } diff --git a/source/core/rendering/shader_backend.hpp b/source/core/rendering/shader_backend.hpp index 16c6786..c823885 100644 --- a/source/core/rendering/shader_backend.hpp +++ b/source/core/rendering/shader_backend.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "utils/defaults.hpp" @@ -90,8 +91,17 @@ namespace Rendering { // Destrueix tots els recursos GPU (pipeline, textura, buffer, sampler, device). virtual void cleanup() = 0; + // Allibera la finestra del device sense destruir el device. Cal cridar-ho + // ABANS de SDL_DestroyWindow per evitar que vkDestroyInstance intente + // alliberar una Wayland surface ja desapareguda. + virtual void releaseWindow() = 0; + [[nodiscard]] virtual auto isReady() const -> bool = 0; [[nodiscard]] virtual auto getDriverName() const -> std::string = 0; }; + // Fàbrica: construeix el backend per defecte del projecte (SDL3 GPU). + // Screen l'invoca sense conèixer la classe concreta. + [[nodiscard]] auto makeDefaultShaderBackend() -> std::unique_ptr; + } // namespace Rendering From 81fe826d175e76d830bf5574ee921bf96aa06327 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:38:11 +0200 Subject: [PATCH 08/19] refactor(core/rendering): Surface desacoblat de Screen via Canvas (COUP-02b / COUP-03) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface::renderWithColorReplace usava Screen::get().getCanvasArgb() i companys per a obtenir el buffer destí. S'introdueix una abstracció mínima: - source/core/rendering/canvas.hpp: struct Canvas amb pixels/width/ height i la fàbrica activeCanvas() que retorna el canvas actiu. - source/core/rendering/canvas.cpp: implementació que delega a Screen. Surface ara inclou canvas.hpp en lloc de screen.hpp i crida Rendering::activeCanvas(). L'indirecció permet, més endavant, empènyer canvases alternatius (off-screen, efectes) sense tocar Surface; ara mateix es comporta idèntic al codi anterior. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- CMakeLists.txt | 1 + docs/audit-core-rendering.md | 7 ++++--- source/core/rendering/canvas.cpp | 16 ++++++++++++++++ source/core/rendering/canvas.hpp | 24 ++++++++++++++++++++++++ source/core/rendering/surface.cpp | 9 +++++---- 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 source/core/rendering/canvas.cpp create mode 100644 source/core/rendering/canvas.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 51cee2a..913c2bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ set(APP_SOURCES source/core/input/key_config.cpp source/core/input/global_inputs.cpp source/core/rendering/screen.cpp + source/core/rendering/canvas.cpp source/core/rendering/notifier.cpp source/core/rendering/render_info.cpp source/core/rendering/gif.cpp diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 45eefed..f0b0d38 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -22,7 +22,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | DUP-02 | `Surface::render` / `renderWithColorReplace` duplicados | DONE | | CONV-03 | Shaders MSL como raw strings en `.cpp` + helpers duplicados | DONE | | DUP-03 | Familia `Text::write*` con bucle UTF-8 repetido 5 veces | DONE | -| COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | WIP (a: DONE, b: PENDING) | +| COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | DONE | --- @@ -73,7 +73,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | COUP-01a | `notifier.hpp:9` | `core/rendering/notifier.hpp` incluye `game/ui/notification_presets.hpp` | Crítico | DONE | helper `Notification::notify()` en `game/ui` | | COUP-01b | `render_info.cpp:12-13` | `core/rendering/render_info.cpp` incluye `game/options.hpp` + `game/ui/ui_palette.hpp` | Crítico | DONE | `RenderInfo::Frame` injectada; Options.buildRenderInfoFrame() | | COUP-02a | `screen.cpp:13-14,109` | `Screen` instancia `SDL3GPUShader` concreto | Importante | DONE | `Rendering::makeDefaultShaderBackend()` factory | -| COUP-03 | `surface.cpp:81-83,110-112` | `Surface::render` usa `Screen::get()` global | Importante | PENDING | pasar destino por parámetro | +| COUP-03 | `surface.cpp:81-83,110-112` | `Surface::render` usa `Screen::get()` global | Importante | DONE | `Rendering::activeCanvas()` intermediari | | COUP-04 | `notifier.cpp:19`, `render_info.cpp:52` | `Resource::Cache::get().getSurface("8bithud.gif")` con string literal | Importante | PENDING | inyectar la surface | | COUP-05 | `animated_sprite.cpp:9,95-116` | parser YAML incrustado en sprite | Importante | PENDING | `AnimationLoader` en `core/resources` | | COUP-06 | `notifier.hpp:22`, `render_info.hpp:17`, `screen.hpp:13` | 3 Meyers singletons en mismo subsistema | Importante | DEFERRED | requiere tocar Director | @@ -125,4 +125,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `d479475` — DUP-02: `Surface::render` delega a `renderWithColorReplace` - `5f46fb2` — CONV-03 + DUP-04 + DUP-05 + PATCH-07: shaders MSL i helpers extrets a `sdl3gpu/msl/` i `sdl3gpu/shader_factory.hpp` - `a63deac` — DUP-03 + HC-01: `Text` amb `resolveGlyph` i constants `INK_COLOR_INDEX` / `DEFAULT_TEXT_COLOR` -- `` — COUP-02a: `Screen` desacoblat de `SDL3GPUShader` via `makeDefaultShaderBackend()` +- `029907d` — COUP-02a: `Screen` desacoblat de `SDL3GPUShader` via `makeDefaultShaderBackend()` +- `` — COUP-02b (= COUP-03): `Surface` usa `Rendering::activeCanvas()` en lloc de `Screen::get()` diff --git a/source/core/rendering/canvas.cpp b/source/core/rendering/canvas.cpp new file mode 100644 index 0000000..008349a --- /dev/null +++ b/source/core/rendering/canvas.cpp @@ -0,0 +1,16 @@ +#include "core/rendering/canvas.hpp" + +#include "core/rendering/screen.hpp" + +namespace Rendering { + + auto activeCanvas() -> Canvas { + auto& screen = Screen::get(); + return { + .pixels = screen.getCanvasArgb(), + .width = screen.getCanvasWidth(), + .height = screen.getCanvasHeight(), + }; + } + +} // namespace Rendering diff --git a/source/core/rendering/canvas.hpp b/source/core/rendering/canvas.hpp new file mode 100644 index 0000000..6e79a83 --- /dev/null +++ b/source/core/rendering/canvas.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include + +// Abstracció mínima del destí de renderitzat CPU. Tots els blitters del +// projecte (Surface, els HUDs via Screen::fillRect, ...) emeten píxels en +// format ARGB8888 sobre una única superfície lògica que el backend GPU +// després transfereix a la textura "scene". L'accés va per la funció +// activeCanvas() en lloc de Screen::get() directament, per a que els +// consumidors no depenguen del cicle de vida de Screen ni del seu header. +namespace Rendering { + + struct Canvas { + std::uint32_t* pixels{nullptr}; + int width{0}; + int height{0}; + }; + + // Retorna el canvas actiu del joc. En aquesta fase del projecte n'hi ha + // sempre un i només un (el de Screen); en el futur, si cal renderitzar + // off-screen (miniatures, efectes), es podrà pushejar/popar per ací. + [[nodiscard]] auto activeCanvas() -> Canvas; + +} // namespace Rendering diff --git a/source/core/rendering/surface.cpp b/source/core/rendering/surface.cpp index f41203b..100c345 100644 --- a/source/core/rendering/surface.cpp +++ b/source/core/rendering/surface.cpp @@ -6,8 +6,8 @@ #include #include +#include "core/rendering/canvas.hpp" #include "core/rendering/gif.hpp" -#include "core/rendering/screen.hpp" #include "core/resources/resource_helper.hpp" #include "utils/utils.hpp" @@ -86,9 +86,10 @@ void Surface::render(int x, int y, const SDL_FRect* src_rect, SDL_FlipMode flip) } void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, const SDL_FRect* src_rect, SDL_FlipMode flip) const { - Uint32* dst = Screen::get().getCanvasArgb(); - const int DST_W = Screen::get().getCanvasWidth(); - const int DST_H = Screen::get().getCanvasHeight(); + const auto CANVAS = Rendering::activeCanvas(); + Uint32* dst = CANVAS.pixels; + const int DST_W = CANVAS.width; + const int DST_H = CANVAS.height; const int SX = (src_rect != nullptr) ? static_cast(src_rect->x) : 0; const int SY = (src_rect != nullptr) ? static_cast(src_rect->y) : 0; From b51109e3809a45ed77bd39a2d2b0e54763dc7df6 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:40:54 +0200 Subject: [PATCH 09/19] refactor(core/rendering): esborrar codi mort de Text (DEAD-01 + DEAD-02) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminats cinc mètodes sense callers externs: - writeShadowed / writeCentered: la funcionalitat viu a writeDX(flags, ...) que és l'API reutilitzable; ningú crida aquestes variants discretes. - glyphWidth / getGlyphClip: getters per glif no consumits enlloc. - codepointToUtf8: helper de serialització UTF-8 no consumit (només es consumeix el parser nextCodepoint, que queda). Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 7 +++--- source/core/rendering/text.cpp | 46 ---------------------------------- source/core/rendering/text.hpp | 6 ----- 3 files changed, 4 insertions(+), 55 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index f0b0d38..7c0b653 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -94,8 +94,8 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| -| DEAD-01 | `text.hpp:50-51` | `writeShadowed`, `writeCentered` sin callers externos | Importante | PENDING | borrar | -| DEAD-02 | `text.hpp:62-63,67` | `glyphWidth`, `getGlyphClip`, `codepointToUtf8` sin callers | Importante | PENDING | borrar | +| DEAD-01 | `text.hpp:50-51` | `writeShadowed`, `writeCentered` sin callers externos | Importante | DONE | esborrats | +| DEAD-02 | `text.hpp:62-63,67` | `glyphWidth`, `getGlyphClip`, `codepointToUtf8` sin callers | Importante | DONE | esborrats | | DEAD-03 | `text.hpp:64` | `getSprite()` sin callers | Menor | PENDING | borrar | | DEAD-04 | `sprite.hpp:33` | `getRect()` sin callers | Menor | PENDING | borrar | | DEAD-05 | `sprite.hpp:42-43` | `incX`, `incY` sin callers | Menor | PENDING | borrar | @@ -126,4 +126,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `5f46fb2` — CONV-03 + DUP-04 + DUP-05 + PATCH-07: shaders MSL i helpers extrets a `sdl3gpu/msl/` i `sdl3gpu/shader_factory.hpp` - `a63deac` — DUP-03 + HC-01: `Text` amb `resolveGlyph` i constants `INK_COLOR_INDEX` / `DEFAULT_TEXT_COLOR` - `029907d` — COUP-02a: `Screen` desacoblat de `SDL3GPUShader` via `makeDefaultShaderBackend()` -- `` — COUP-02b (= COUP-03): `Surface` usa `Rendering::activeCanvas()` en lloc de `Screen::get()` +- `81fe826` — COUP-02b (= COUP-03): `Surface` usa `Rendering::activeCanvas()` en lloc de `Screen::get()` +- `` — DEAD-01 + DEAD-02: esborrats `writeShadowed`/`writeCentered`/`glyphWidth`/`getGlyphClip`/`codepointToUtf8` diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index 1f40c73..fe53872 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -47,26 +47,6 @@ auto Text::nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_ return cp; } -auto Text::codepointToUtf8(std::uint32_t cp) -> std::string { - std::string result; - if (cp < 0x80) { - result += static_cast(cp); - } else if (cp < 0x800) { - result += static_cast(0xC0 | (cp >> 6)); - result += static_cast(0x80 | (cp & 0x3F)); - } else if (cp < 0x10000) { - result += static_cast(0xE0 | (cp >> 12)); - result += static_cast(0x80 | ((cp >> 6) & 0x3F)); - result += static_cast(0x80 | (cp & 0x3F)); - } else { - result += static_cast(0xF0 | (cp >> 18)); - result += static_cast(0x80 | ((cp >> 12) & 0x3F)); - result += static_cast(0x80 | ((cp >> 6) & 0x3F)); - result += static_cast(0x80 | (cp & 0x3F)); - } - return result; -} - // Format .fnt: claus "box_width/box_height/columns/cell_spacing/row_spacing" // i una línia per glif amb "codepoint_decimal amplada_visual". Els glifs // s'ordenen al png per ordre d'aparició al .fnt (glyph_index). @@ -165,16 +145,6 @@ void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int } } -void Text::writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance, int kerning, int length) { - writeColored(x + shadow_distance, y + shadow_distance, text, color, kerning, length); - write(x, y, text, kerning, length); -} - -void Text::writeCentered(int x, int y, const std::string& text, int kerning, int length) { - x -= (Text::length(text, kerning) / 2); - write(x, y, text, kerning, length); -} - void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int length) { const bool CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG); const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG); @@ -265,22 +235,6 @@ auto Text::length(const std::string& text, int kerning) const -> int { return shift > 0 ? shift - kerning : 0; } -auto Text::glyphWidth(std::uint32_t codepoint, int kerning) const -> int { - if (const Offset* off = resolveGlyph(codepoint); off != nullptr) { - return off->w + kerning; - } - return 0; -} - -auto Text::getGlyphClip(std::uint32_t codepoint) const -> SDL_FRect { - const Offset* off = resolveGlyph(codepoint); - if (off == nullptr) { return {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; } - return {.x = static_cast(off->x), - .y = static_cast(off->y), - .w = static_cast(box_width_), - .h = static_cast(box_height_)}; -} - auto Text::resolveGlyph(std::uint32_t cp) const -> const Offset* { auto it = offset_.find(cp); if (it == offset_.end()) { it = offset_.find('?'); } diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp index a65c023..6dac085 100644 --- a/source/core/rendering/text.hpp +++ b/source/core/rendering/text.hpp @@ -54,8 +54,6 @@ class Text { void write(int x, int y, const std::string& text, int kerning = 1, int length = -1); void writeColored(int x, int y, const std::string& text, Uint8 color, int kerning = 1, int length = -1); - void writeShadowed(int x, int y, const std::string& text, Uint8 color, Uint8 shadow_distance = 1, int kerning = 1, int length = -1); - void writeCentered(int x, int y, const std::string& text, int kerning = 1, int length = -1); void writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning = 1, Uint8 text_color = 0, Uint8 shadow_distance = 1, Uint8 shadow_color = 0, int length = -1); // Dibuixa amb amplada fixa només per als dígits '0'-'9' (cadascun centrat @@ -66,12 +64,8 @@ class Text { [[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; [[nodiscard]] auto lengthMonoDigits(const std::string& text, int digit_cell_w, int kerning = 1) const -> int; [[nodiscard]] auto getCharacterSize() const -> int { return box_width_; } - [[nodiscard]] auto glyphWidth(std::uint32_t codepoint, int kerning = 0) const -> int; - [[nodiscard]] auto getGlyphClip(std::uint32_t codepoint) const -> SDL_FRect; - [[nodiscard]] auto getSprite() const -> Sprite* { return sprite_.get(); } static auto loadTextFile(const std::string& file_path) -> std::shared_ptr; - static auto codepointToUtf8(std::uint32_t cp) -> std::string; static auto nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t; private: From 597402aa1bf7c4d0fa44d393e20037d3c126a7c2 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:43:39 +0200 Subject: [PATCH 10/19] refactor(core/rendering): esborrar codi mort a Sprite/Surface i inline Sprite::update (DEAD-04/-05/-07/-08/-10 + PATCH-04) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminats mètodes sense callers: - Sprite::getRect() (DEAD-04) - Sprite::incX / incY (DEAD-05) - AnimatedSprite::getCurrentAnimationSize() (DEAD-07) - Surface::getSurfaceData() (DEAD-08) Sprite::update passa a ser virtual amb cos buit inline al header, en lloc de la definició a .cpp amb (void)delta_time; desapareix una línia de "parxejament" sense alterar la polimorfia (Player::update, heretat via AnimatedSprite/MovingSprite, continua fent override correctament). Això tanca DEAD-10 i PATCH-04 alhora. Els ctors per defecte Sprite()/MovingSprite() s'apunten com WONTFIX al document: són necessaris per a la cadena de construcció base d'Animated Sprite (que no delega explícitament al ctor parametritzat). Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 19 ++++++++++--------- .../core/rendering/sprite/animated_sprite.hpp | 1 - source/core/rendering/sprite/sprite.cpp | 4 ---- source/core/rendering/sprite/sprite.hpp | 7 +++---- source/core/rendering/surface.hpp | 1 - 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 7c0b653..f523660 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -60,7 +60,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | PATCH-01 | `screen.cpp:71-92,210-214`, `sdl3gpu_shader.cpp:445-493` | workarounds Wayland/Vulkan | Importante | WONTFIX (de momento) | RAII para forzar orden — deferred | | PATCH-02 | `render_info.cpp:85-88` | guard bug-fix animation | Menor | PENDING | refactor FSM animación | | PATCH-03 | `notifier.cpp:78-85` | TODO implícito de cola FIFO | Menor | WONTFIX | documentar decisión | -| PATCH-04 | `sprite.cpp:47-49` | `Sprite::update` vacío hook no invocado | Menor | PENDING | borrar (ver DEAD-10) | +| PATCH-04 | `sprite.cpp:47-49` | `Sprite::update` vacío hook no invocado | Menor | DONE | resolt amb DEAD-10 | | PATCH-05 | `gif.cpp:12-15` | comentario "reemplaçar macro READ" | Menor | PENDING | limpiar comentario | | PATCH-06 | `gif.cpp:83-86` | guard self-reference defensivo | Menor | WONTFIX | dejar, cambiar `cerr` → logger futuro | | PATCH-07 | `sdl3gpu_shader.cpp:17,162` + `crtpi_pipeline.cpp:16,162` | `NOLINTBEGIN` gigantes sobre MSL | Importante | DONE | eliminats amb CONV-03 | @@ -96,14 +96,14 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic |----|---------------|----------|-----------|--------|-------| | DEAD-01 | `text.hpp:50-51` | `writeShadowed`, `writeCentered` sin callers externos | Importante | DONE | esborrats | | DEAD-02 | `text.hpp:62-63,67` | `glyphWidth`, `getGlyphClip`, `codepointToUtf8` sin callers | Importante | DONE | esborrats | -| DEAD-03 | `text.hpp:64` | `getSprite()` sin callers | Menor | PENDING | borrar | -| DEAD-04 | `sprite.hpp:33` | `getRect()` sin callers | Menor | PENDING | borrar | -| DEAD-05 | `sprite.hpp:42-43` | `incX`, `incY` sin callers | Menor | PENDING | borrar | -| DEAD-06 | `sprite.hpp:12`, `moving_sprite.hpp:13` | ctors default sin callers | Menor | PENDING | borrar | -| DEAD-07 | `animated_sprite.hpp:30` | `getCurrentAnimationSize()` sin callers | Menor | PENDING | borrar | -| DEAD-08 | `surface.hpp:61` | `getSurfaceData()` sin callers externos | Menor | PENDING | borrar o privatizar | +| DEAD-03 | `text.hpp:64` | `getSprite()` sin callers | Menor | DONE | esborrat amb DEAD-01/02 | +| DEAD-04 | `sprite.hpp:33` | `getRect()` sin callers | Menor | DONE | esborrat | +| DEAD-05 | `sprite.hpp:42-43` | `incX`, `incY` sin callers | Menor | DONE | esborrats | +| DEAD-06 | `sprite.hpp:12`, `moving_sprite.hpp:13` | ctors default sin callers | Menor | WONTFIX | usats transitivament pel ctor de `AnimatedSprite` (base-class-construction) | +| DEAD-07 | `animated_sprite.hpp:30` | `getCurrentAnimationSize()` sin callers | Menor | DONE | esborrat | +| DEAD-08 | `surface.hpp:61` | `getSurfaceData()` sin callers externos | Menor | DONE | esborrat | | DEAD-09 | `gif.hpp:45-65` | `Extension`, `ApplicationExtension`, `PlaintextExtension`, `GraphicControlExtension` no instanciados | Menor | PENDING | borrar | -| DEAD-10 | `sprite.cpp:47-49` | `Sprite::update` hook no invocado | Menor | PENDING | borrar (cruza PATCH-04) | +| DEAD-10 | `sprite.cpp:47-49` | `Sprite::update` hook no invocado | Menor | DONE | inlined buit al header (resol PATCH-04) | ## 7. Violaciones de convenciones del proyecto @@ -127,4 +127,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `a63deac` — DUP-03 + HC-01: `Text` amb `resolveGlyph` i constants `INK_COLOR_INDEX` / `DEFAULT_TEXT_COLOR` - `029907d` — COUP-02a: `Screen` desacoblat de `SDL3GPUShader` via `makeDefaultShaderBackend()` - `81fe826` — COUP-02b (= COUP-03): `Surface` usa `Rendering::activeCanvas()` en lloc de `Screen::get()` -- `` — DEAD-01 + DEAD-02: esborrats `writeShadowed`/`writeCentered`/`glyphWidth`/`getGlyphClip`/`codepointToUtf8` +- `b51109e` — DEAD-01 + DEAD-02 + DEAD-03: esborrats mètodes mort de `Text` +- `` — DEAD-04/05/07/08/10 + PATCH-04: neteja mort a `Sprite`/`AnimatedSprite`/`Surface` + `Sprite::update` inline diff --git a/source/core/rendering/sprite/animated_sprite.hpp b/source/core/rendering/sprite/animated_sprite.hpp index bead9ac..b05cd38 100644 --- a/source/core/rendering/sprite/animated_sprite.hpp +++ b/source/core/rendering/sprite/animated_sprite.hpp @@ -27,7 +27,6 @@ class AnimatedSprite : public MovingSprite { void update(float delta_time) override; auto animationIsCompleted() -> bool; - auto getCurrentAnimationSize() -> int { return static_cast(animations_[current_animation_].frames.size()); } void setCurrentAnimation(const std::string& name = "default"); void setCurrentAnimation(int index = 0); diff --git a/source/core/rendering/sprite/sprite.cpp b/source/core/rendering/sprite/sprite.cpp index 02579c9..2e04939 100644 --- a/source/core/rendering/sprite/sprite.cpp +++ b/source/core/rendering/sprite/sprite.cpp @@ -43,7 +43,3 @@ void Sprite::clear() { pos_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; clip_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; } - -void Sprite::update(float delta_time) { - (void)delta_time; -} diff --git a/source/core/rendering/sprite/sprite.hpp b/source/core/rendering/sprite/sprite.hpp index c73d043..806b2d5 100644 --- a/source/core/rendering/sprite/sprite.hpp +++ b/source/core/rendering/sprite/sprite.hpp @@ -16,7 +16,9 @@ class Sprite { virtual ~Sprite() = default; - virtual void update(float delta_time); + // Hook d'actualització per frame. Base buida (no hi ha Sprite pur a + // actualitzar); les subclasses mòbils/animades fan override. + virtual void update(float /*delta_time*/) {} virtual void render(); // Renderitza substituint l'índex source_color per target_color (ús típic: // la classe Text, que recolora glifs monocroms per paràmetre). @@ -30,7 +32,6 @@ class Sprite { [[nodiscard]] auto getPosition() const -> SDL_FRect { return pos_; } [[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; } [[nodiscard]] auto getSurface() const -> std::shared_ptr { return surface_; } - auto getRect() -> SDL_FRect& { return pos_; } void setX(float x) { pos_.x = x; } void setY(float y) { pos_.y = y; } @@ -39,8 +40,6 @@ class Sprite { void setPosition(float x, float y); void setPosition(SDL_FPoint p); void setPosition(SDL_FRect r) { pos_ = r; } - void incX(float value) { pos_.x += value; } - void incY(float value) { pos_.y += value; } void setClip(SDL_FRect rect) { clip_ = rect; } void setClip(float x, float y, float w, float h) { clip_ = SDL_FRect{.x = x, .y = y, .w = w, .h = h}; } diff --git a/source/core/rendering/surface.hpp b/source/core/rendering/surface.hpp index 0a9ef16..ee1aa3b 100644 --- a/source/core/rendering/surface.hpp +++ b/source/core/rendering/surface.hpp @@ -58,7 +58,6 @@ class Surface { void putPixel(int x, int y, Uint8 color); [[nodiscard]] auto getPixel(int x, int y) const -> Uint8; - [[nodiscard]] auto getSurfaceData() const -> std::shared_ptr { return surface_data_; } [[nodiscard]] auto getWidth() const -> int { return surface_data_->width; } [[nodiscard]] auto getHeight() const -> int { return surface_data_->height; } From ce3b6da6170421a64e96962078a27f9854aa1edc Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:44:48 +0200 Subject: [PATCH 11/19] refactor(core/rendering): esborrar structs d'extensions GIF no usats (DEAD-09) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gif.hpp declarava Extension, GraphicControlExtension, ApplicationExtension i PlaintextExtension com a tipus "empaquetats" per a llegir blocs del stream GIF. El parser a gif.cpp, però, processa els bytes cru (avança el punter i llig block_size + sub-blocs manualment) sense instanciar-los mai. Les struct eren code pattern orientat a reus futurs que no va arribar a materialitzar-se. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 5 +++-- source/core/rendering/gif.hpp | 22 ---------------------- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index f523660..4321ec7 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -102,7 +102,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | DEAD-06 | `sprite.hpp:12`, `moving_sprite.hpp:13` | ctors default sin callers | Menor | WONTFIX | usats transitivament pel ctor de `AnimatedSprite` (base-class-construction) | | DEAD-07 | `animated_sprite.hpp:30` | `getCurrentAnimationSize()` sin callers | Menor | DONE | esborrat | | DEAD-08 | `surface.hpp:61` | `getSurfaceData()` sin callers externos | Menor | DONE | esborrat | -| DEAD-09 | `gif.hpp:45-65` | `Extension`, `ApplicationExtension`, `PlaintextExtension`, `GraphicControlExtension` no instanciados | Menor | PENDING | borrar | +| DEAD-09 | `gif.hpp:45-65` | `Extension`, `ApplicationExtension`, `PlaintextExtension`, `GraphicControlExtension` no instanciados | Menor | DONE | esborrats (el parser processa bytes cru) | | DEAD-10 | `sprite.cpp:47-49` | `Sprite::update` hook no invocado | Menor | DONE | inlined buit al header (resol PATCH-04) | ## 7. Violaciones de convenciones del proyecto @@ -128,4 +128,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `029907d` — COUP-02a: `Screen` desacoblat de `SDL3GPUShader` via `makeDefaultShaderBackend()` - `81fe826` — COUP-02b (= COUP-03): `Surface` usa `Rendering::activeCanvas()` en lloc de `Screen::get()` - `b51109e` — DEAD-01 + DEAD-02 + DEAD-03: esborrats mètodes mort de `Text` -- `` — DEAD-04/05/07/08/10 + PATCH-04: neteja mort a `Sprite`/`AnimatedSprite`/`Surface` + `Sprite::update` inline +- `597402a` — DEAD-04/05/07/08/10 + PATCH-04: neteja mort a `Sprite`/`AnimatedSprite`/`Surface` + `Sprite::update` inline +- `` — DEAD-09: esborrats structs d'extensions GIF no usats diff --git a/source/core/rendering/gif.hpp b/source/core/rendering/gif.hpp index b6443f1..c1e30a6 100644 --- a/source/core/rendering/gif.hpp +++ b/source/core/rendering/gif.hpp @@ -42,28 +42,6 @@ namespace GIF { int len; }; - struct Extension { - uint8_t extension_code; - uint8_t block_size; - }; - - struct GraphicControlExtension { - uint8_t fields; - uint16_t delay_time; - uint8_t transparent_color_index; - }; - - struct ApplicationExtension { - uint8_t application_id[8]; - uint8_t version[3]; - }; - - struct PlaintextExtension { - uint16_t left, top, width, height; - uint8_t cell_width, cell_height; - uint8_t foreground_color, background_color; - }; - class Gif { public: // Descompone (uncompress) el bloque comprimido usando LZW. From bbb397abb2cc9679ce1f79a82c766cc1c54a6002 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:47:49 +0200 Subject: [PATCH 12/19] refactor(core/rendering): nomenar constants a RenderInfo (HC-06/-07/-09 + PATCH-08) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit render_info.cpp tenia diverses "cifres soltes" amb significat estructural: - Índex 0/1/2/3 dins segments_ per a FPS/driver/shader/temps — ara FPS_SEGMENT, DRIVER_SEGMENT, SHADER_SEGMENT, TIME_SEGMENT (HC-06). - +1,+1 per a la ombra del text — ara SHADOW_OFFSET_PX (HC-09). - % 3 al cicle de posicions de global_inputs.cpp — ara Defaults::RENDER_INFO_POSITION_COUNT (HC-07). A més, init() assignava mono_digits=true al segment de temps que refreshSegments() ja marca a cada crida, inclosa la primera d'update() abans de render. S'elimina el doblegat (PATCH-08); el comentari explica la garantia. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 11 +++--- source/core/input/global_inputs.cpp | 2 +- source/core/rendering/render_info.cpp | 53 +++++++++++++++------------ source/utils/defaults.hpp | 1 + 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 4321ec7..3d8f1eb 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -47,10 +47,10 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | HC-03 | `gif.cpp:71,95,…` | LZW `<11`/`<12`/`<2` y máscaras `0x80/0x70/0x07…` sin nombre | Menor | PENDING | constantes con nombre | | HC-04 | `notifier.hpp:41-46`, `render_info.hpp:38-40` | HUD constants hardcoded en clase, no en `defaults`/YAML | Importante | PENDING | `Defaults::Hud::…` o config.yaml | | HC-05 | `render_info.cpp:19` + `notifier.hpp:43` | `TEXT_SIZE=8` duplicado | Menor | PENDING | constante global font | -| HC-06 | `render_info.cpp:64,138-139,193-195` | `segments_[3]` hardcode del segmento tiempo | Importante | PENDING | `enum SegmentId` | -| HC-07 | `render_info.cpp:207` | `% 3` hardcode nº de entradas enum | Menor | PENDING | `RENDER_INFO_POSITION_COUNT` | +| HC-06 | `render_info.cpp:64,138-139,193-195` | `segments_[3]` hardcode del segmento tiempo | Importante | DONE | constants `FPS/DRIVER/SHADER/TIME_SEGMENT` | +| HC-07 | `render_info.cpp:207` | `% 3` hardcode nº de entradas enum | Menor | DONE | `Defaults::RENDER_INFO_POSITION_COUNT` | | HC-08 | `crtpi_pipeline.cpp:97` | `768.0f / h / 3.0f` magic en shader | Menor | PENDING | uniforme o comentario | -| HC-09 | `render_info.cpp:179-183,194-195` | shadow offset +1,+1 magic | Menor | PENDING | `SHADOW_DIST` | +| HC-09 | `render_info.cpp:179-183,194-195` | shadow offset +1,+1 magic | Menor | DONE | `SHADOW_OFFSET_PX` | | HC-10 | `gif.cpp:162` | `processImageDescriptor` params `[[maybe_unused]]` vestigiales | Menor | PENDING | quitar args | ## 3. Parches e improvisaciones @@ -64,7 +64,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | PATCH-05 | `gif.cpp:12-15` | comentario "reemplaçar macro READ" | Menor | PENDING | limpiar comentario | | PATCH-06 | `gif.cpp:83-86` | guard self-reference defensivo | Menor | WONTFIX | dejar, cambiar `cerr` → logger futuro | | PATCH-07 | `sdl3gpu_shader.cpp:17,162` + `crtpi_pipeline.cpp:16,162` | `NOLINTBEGIN` gigantes sobre MSL | Importante | DONE | eliminats amb CONV-03 | -| PATCH-08 | `render_info.cpp:64,138-139` | `mono_digits=true` asignado 2 veces | Menor | PENDING | init única | +| PATCH-08 | `render_info.cpp:64,138-139` | `mono_digits=true` asignado 2 veces | Menor | DONE | inicialització única via refreshSegments | ## 4. Acoplamiento innecesario @@ -129,4 +129,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `81fe826` — COUP-02b (= COUP-03): `Surface` usa `Rendering::activeCanvas()` en lloc de `Screen::get()` - `b51109e` — DEAD-01 + DEAD-02 + DEAD-03: esborrats mètodes mort de `Text` - `597402a` — DEAD-04/05/07/08/10 + PATCH-04: neteja mort a `Sprite`/`AnimatedSprite`/`Surface` + `Sprite::update` inline -- `` — DEAD-09: esborrats structs d'extensions GIF no usats +- `ce3b6da` — DEAD-09: esborrats structs d'extensions GIF no usats +- `` — HC-06 + HC-07 + HC-09 + PATCH-08: constants nominals a `render_info` (segments, shadow offset, position count) i dedup init diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index a365b90..315ac00 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -139,7 +139,7 @@ namespace GlobalInputs { } void onCycleRenderInfo() { - const int NEXT = (static_cast(Options::video.render_info_position) + 1) % 3; + const int NEXT = (static_cast(Options::video.render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT; Options::video.render_info_position = static_cast(NEXT); Options::saveToFile(); } diff --git a/source/core/rendering/render_info.cpp b/source/core/rendering/render_info.cpp index ab5ef75..738f39f 100644 --- a/source/core/rendering/render_info.cpp +++ b/source/core/rendering/render_info.cpp @@ -21,8 +21,14 @@ namespace { // Evita una fileta d'un sol píxel quan acaba de començar a aparèixer. constexpr float VISIBLE_EPSILON = 0.01F; - // Índex del segment de temps dins de segments_. Si canvia SEGMENT_COUNT - // o l'ordre, actualitzar ací. + // Desplaçament en píxels de la ombra respecte del text principal. + constexpr int SHADOW_OFFSET_PX = 1; + + // Índex de cada segment dins de segments_. Si canvia SEGMENT_COUNT o + // l'ordre, actualitzar ací. + constexpr int FPS_SEGMENT = 0; + constexpr int DRIVER_SEGMENT = 1; + constexpr int SHADER_SEGMENT = 2; constexpr int TIME_SEGMENT = 3; auto prettify(std::string s) -> std::string { @@ -55,10 +61,11 @@ void RenderInfo::init(const Frame& initial) { text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); current_ = initial; - segments_[TIME_SEGMENT].mono_digits = true; // Restaura l'estat guardat: si l'overlay estava engegat, l'activem ja - // plenament visible (sense animació d'entrada inicial). + // plenament visible (sense animació d'entrada inicial). refreshSegments + // és qui marca mono_digits al segment de temps; si no s'engega ara, + // la primera crida a update() també ho farà abans de qualsevol render. if (initial.target_position != Defaults::RenderInfoPosition::OFF) { visible_pos_ = initial.target_position; info_anim_ = 1.0F; @@ -111,35 +118,35 @@ void RenderInfo::update(int delta_ms, const Frame& frame) { } void RenderInfo::refreshSegments() { - // [0] FPS (sempre visible mentre l'overlay apareix) - segments_[0].text = std::to_string(Screen::get().getLastFPS()) + " fps"; - segments_[0].visible = true; - segments_[0].mono_digits = false; + // FPS (sempre visible mentre l'overlay apareix) + segments_[FPS_SEGMENT].text = std::to_string(Screen::get().getLastFPS()) + " fps"; + segments_[FPS_SEGMENT].visible = true; + segments_[FPS_SEGMENT].mono_digits = false; - // [1] Driver GPU (sempre visible) + // Driver GPU (sempre visible) const auto& driver = Screen::get().getGPUDriver(); - segments_[1].text = std::string(" | ") + (driver.empty() ? std::string("sdl") : driver); - segments_[1].visible = true; - segments_[1].mono_digits = false; + segments_[DRIVER_SEGMENT].text = std::string(" | ") + (driver.empty() ? std::string("sdl") : driver); + segments_[DRIVER_SEGMENT].visible = true; + segments_[DRIVER_SEGMENT].mono_digits = false; - // [2] Shader + preset (només amb shader activat). El caller ha triat el + // Shader + preset (només amb shader activat). El caller ha triat el // preset actiu segons el shader; ací només el formatem. if (current_.shader_enabled) { - segments_[2].text = " | " + current_.shader_name + " " + prettify(current_.shader_preset); + segments_[SHADER_SEGMENT].text = " | " + current_.shader_name + " " + prettify(current_.shader_preset); } - segments_[2].visible = current_.shader_enabled; - segments_[2].mono_digits = false; + segments_[SHADER_SEGMENT].visible = current_.shader_enabled; + segments_[SHADER_SEGMENT].mono_digits = false; - // [3] Temps de joc (togglejable i persistent) + // Temps de joc (togglejable i persistent) segments_[TIME_SEGMENT].text = " | " + formatTime(elapsed_ms_); segments_[TIME_SEGMENT].visible = current_.show_time; segments_[TIME_SEGMENT].mono_digits = true; // Els tres primers segments baixem a minúscules per coherència estètica - // amb la resta de la línia; el [3] (temps) manté dígits i puntuació. - segments_[0].text = prettify(segments_[0].text); - segments_[1].text = prettify(segments_[1].text); - segments_[2].text = prettify(segments_[2].text); + // amb la resta de la línia; el de temps manté dígits i puntuació. + segments_[FPS_SEGMENT].text = prettify(segments_[FPS_SEGMENT].text); + segments_[DRIVER_SEGMENT].text = prettify(segments_[DRIVER_SEGMENT].text); + segments_[SHADER_SEGMENT].text = prettify(segments_[SHADER_SEGMENT].text); } auto RenderInfo::segmentWidth(const Segment& seg, int digit_cell_w) const -> int { @@ -188,10 +195,10 @@ void RenderInfo::render() { const int SEG_W = segmentWidth(seg, DIGIT_CELL); if (seg.mono_digits) { - text_->writeMonoDigits(XI + 1, Y + 1, seg.text, current_.shadow_color, DIGIT_CELL); + text_->writeMonoDigits(XI + SHADOW_OFFSET_PX, Y + SHADOW_OFFSET_PX, seg.text, current_.shadow_color, DIGIT_CELL); text_->writeMonoDigits(XI, Y, seg.text, current_.text_color, DIGIT_CELL); } else { - text_->writeColored(XI + 1, Y + 1, seg.text, current_.shadow_color); + text_->writeColored(XI + SHADOW_OFFSET_PX, Y + SHADOW_OFFSET_PX, seg.text, current_.shadow_color); text_->writeColored(XI, Y, seg.text, current_.text_color); } diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index e2939e1..6211af6 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -30,6 +30,7 @@ namespace Defaults { TOP = 1, BOTTOM = 2, }; + constexpr int RENDER_INFO_POSITION_COUNT = 3; namespace Window { constexpr int ZOOM = 3; From be94a4390e859008c5ba56b8134195f711a76a04 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:53:43 +0200 Subject: [PATCH 13/19] =?UTF-8?q?refactor(core/rendering):=20neteja=20del?= =?UTF-8?q?=20parser=20GIF=20i=20construcci=C3=B3=20de=20Surface=20(DUP-01?= =?UTF-8?q?/-06=20+=20HC-03/-10=20+=20PATCH-05)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DUP-01: les tres branques del switch d'extensions que saltaven el mateix (bloc fix + sub-blocs) es substitueixen per una crida a skipExtensionBlock(); com que totes les extensions comparteixen layout, el switch desapareix i el codi es redueix a una línia. - DUP-06: el constructor Surface(file_path) llegia el fitxer dues voltes (una via loadSurface per pixels i una altra via loadPaletteFromGif per la paleta); ara carrega el buffer una sola vegada i n'extrau pixels + paleta en passades separades sobre la memòria. Surface::loadSurface passa a no existir públicament (no tenia callers externs). - HC-03: les màscares i talls màgics del decoder LZW i del Screen Descriptor passen a constants nominals (LZW_MIN/MAX_CODE_LENGTH, LZW_MASK_INITIAL, LZW_MASK_OVERFLOW, SCREEN_GCT_FLAG, SCREEN_GCT_SIZE_MASK, GCT_BYTES_PER_ENTRY). - HC-10: processImageDescriptor tenia gct i resolution_bits [[maybe_unused]] vestigials; se'n treu la signatura. L'struct RGB, que només servia per a aquests args, també s'elimina. - PATCH-05: el comentari "reemplaçar macro READ" (migració parcial des de C) passa a ser un comentari neutre sobre el que fa readBytes. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 13 +-- source/core/rendering/gif.cpp | 133 +++++++++++++----------------- source/core/rendering/gif.hpp | 6 +- source/core/rendering/surface.cpp | 19 +++-- source/core/rendering/surface.hpp | 2 - 5 files changed, 78 insertions(+), 95 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 3d8f1eb..123d102 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -30,12 +30,12 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| -| DUP-01 | `gif.cpp:238-276` | 3 ramas del switch extension saltan bloque+subloops idénticos | Importante | PENDING | helper `skipExtensionBlocks` | +| DUP-01 | `gif.cpp:238-276` | 3 ramas del switch extension saltan bloque+subloops idénticos | Importante | DONE | `skipExtensionBlock` i switch eliminat | | DUP-02 | `surface.cpp:80-137` | `render` vs `renderWithColorReplace` ~90% idénticos | Crítico | DONE | `render()` delega amb source==target==transparent | | DUP-03 | `text.cpp:145-289` | bucle UTF-8 repetido en write/writeColored/writeMonoDigits/length/lengthMonoDigits | Importante | DONE | `resolveGlyph` + `write` delega a `writeColored` | | DUP-04 | `sdl3gpu_shader.cpp:165-201` + `crtpi_pipeline.cpp:165-202` | `createShaderMSL/SPIRV` duplicados | Importante | DONE | `sdl3gpu/shader_factory.hpp` | | DUP-05 | `sdl3gpu_shader.cpp:18-35` + `crtpi_pipeline.cpp:17-34` | vertex shader MSL duplicado | Menor | DONE | `sdl3gpu/msl/postfx_vert.msl.h` compartit | -| DUP-06 | `surface.cpp:14-43` | constructor Surface abre y parsea GIF 2 veces | Importante | PENDING | `loadGif` devuelve pixels+palette | +| DUP-06 | `surface.cpp:14-43` | constructor Surface abre y parsea GIF 2 veces | Importante | DONE | buffer carregat una volta, pixels+palette del mateix | | DUP-07 | `crtpi_pipeline.hpp:70-87` + `shader_backend.hpp:30-45` + `crtpi_pipeline.cpp:45-62` | defaults CRT-Pi triples | Importante | PENDING | single source of truth | ## 2. Hardcoding @@ -44,14 +44,14 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic |----|---------------|----------|-----------|--------|-------| | HC-01 | `text.cpp:159` | `render(1, 15)` — `1` (tinta) y `15` (color por defecto) literales | Importante | DONE | `Text::INK_COLOR_INDEX` i `DEFAULT_TEXT_COLOR` | | HC-02 | `surface.cpp:29`, `screen.cpp:56,218,244` | `0xFF000000U`/`0xFF` alfa repetido | Menor | PENDING | `OPAQUE_ALPHA` | -| HC-03 | `gif.cpp:71,95,…` | LZW `<11`/`<12`/`<2` y máscaras `0x80/0x70/0x07…` sin nombre | Menor | PENDING | constantes con nombre | +| HC-03 | `gif.cpp:71,95,…` | LZW `<11`/`<12`/`<2` y máscaras `0x80/0x70/0x07…` sin nombre | Menor | DONE | `LZW_MIN/MAX_CODE_LENGTH`, `SCREEN_GCT_FLAG`, `SCREEN_GCT_SIZE_MASK`, etc. | | HC-04 | `notifier.hpp:41-46`, `render_info.hpp:38-40` | HUD constants hardcoded en clase, no en `defaults`/YAML | Importante | PENDING | `Defaults::Hud::…` o config.yaml | | HC-05 | `render_info.cpp:19` + `notifier.hpp:43` | `TEXT_SIZE=8` duplicado | Menor | PENDING | constante global font | | HC-06 | `render_info.cpp:64,138-139,193-195` | `segments_[3]` hardcode del segmento tiempo | Importante | DONE | constants `FPS/DRIVER/SHADER/TIME_SEGMENT` | | HC-07 | `render_info.cpp:207` | `% 3` hardcode nº de entradas enum | Menor | DONE | `Defaults::RENDER_INFO_POSITION_COUNT` | | HC-08 | `crtpi_pipeline.cpp:97` | `768.0f / h / 3.0f` magic en shader | Menor | PENDING | uniforme o comentario | | HC-09 | `render_info.cpp:179-183,194-195` | shadow offset +1,+1 magic | Menor | DONE | `SHADOW_OFFSET_PX` | -| HC-10 | `gif.cpp:162` | `processImageDescriptor` params `[[maybe_unused]]` vestigiales | Menor | PENDING | quitar args | +| HC-10 | `gif.cpp:162` | `processImageDescriptor` params `[[maybe_unused]]` vestigiales | Menor | DONE | signatura sense `gct` ni `resolution_bits`; `RGB` també esborrat | ## 3. Parches e improvisaciones @@ -61,7 +61,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | PATCH-02 | `render_info.cpp:85-88` | guard bug-fix animation | Menor | PENDING | refactor FSM animación | | PATCH-03 | `notifier.cpp:78-85` | TODO implícito de cola FIFO | Menor | WONTFIX | documentar decisión | | PATCH-04 | `sprite.cpp:47-49` | `Sprite::update` vacío hook no invocado | Menor | DONE | resolt amb DEAD-10 | -| PATCH-05 | `gif.cpp:12-15` | comentario "reemplaçar macro READ" | Menor | PENDING | limpiar comentario | +| PATCH-05 | `gif.cpp:12-15` | comentario "reemplaçar macro READ" | Menor | DONE | comentari neutre nou | | PATCH-06 | `gif.cpp:83-86` | guard self-reference defensivo | Menor | WONTFIX | dejar, cambiar `cerr` → logger futuro | | PATCH-07 | `sdl3gpu_shader.cpp:17,162` + `crtpi_pipeline.cpp:16,162` | `NOLINTBEGIN` gigantes sobre MSL | Importante | DONE | eliminats amb CONV-03 | | PATCH-08 | `render_info.cpp:64,138-139` | `mono_digits=true` asignado 2 veces | Menor | DONE | inicialització única via refreshSegments | @@ -130,4 +130,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `b51109e` — DEAD-01 + DEAD-02 + DEAD-03: esborrats mètodes mort de `Text` - `597402a` — DEAD-04/05/07/08/10 + PATCH-04: neteja mort a `Sprite`/`AnimatedSprite`/`Surface` + `Sprite::update` inline - `ce3b6da` — DEAD-09: esborrats structs d'extensions GIF no usats -- `` — HC-06 + HC-07 + HC-09 + PATCH-08: constants nominals a `render_info` (segments, shadow offset, position count) i dedup init +- `bbb397a` — HC-06 + HC-07 + HC-09 + PATCH-08: constants nominals a `render_info` (segments, shadow offset, position count) i dedup init +- `` — DUP-01 + DUP-06 + HC-03 + HC-10 + PATCH-05: neteja parser GIF (extension single-skip, constants nominals LZW/GCT, signatura sense args morts, un sol parse per construcció de `Surface`) diff --git a/source/core/rendering/gif.cpp b/source/core/rendering/gif.cpp index e39cd7f..c3182f1 100644 --- a/source/core/rendering/gif.cpp +++ b/source/core/rendering/gif.cpp @@ -1,19 +1,46 @@ #include "core/rendering/gif.hpp" -#include // Para memcpy, size_t -#include // Para std::cout -#include // Para runtime_error -#include // Para allocator, char_traits, operator==, basic_string +#include +#include +#include +#include namespace GIF { - // Función inline para reemplazar el macro READ. - // Actualiza el puntero 'buffer' tras copiar 'size' bytes a 'dst'. + namespace { + // Paràmetres del Screen Descriptor (byte de flags). + constexpr uint8_t SCREEN_GCT_FLAG = 0x80; + constexpr uint8_t SCREEN_GCT_SIZE_MASK = 0x07; + + // Paràmetres del decompressor LZW. + constexpr int LZW_MIN_CODE_LENGTH = 2; + constexpr int LZW_MAX_CODE_LENGTH = 12; + constexpr unsigned int LZW_MASK_INITIAL = 0x01; + constexpr unsigned int LZW_MASK_OVERFLOW = 0x100; + + // Mida de l'RGB de la GCT (bytes per entrada). + constexpr int GCT_BYTES_PER_ENTRY = 3; + } // namespace + + // Copia 'size' bytes del buffer a 'dst' avançant el punter del buffer. inline void readBytes(const uint8_t*& buffer, void* dst, size_t size) { std::memcpy(dst, buffer, size); buffer += size; } + // Salta un bloc d'extensió: byte de mida + contingut fix + sub-blocs + // acabats amb mida 0. Totes les extensions del GIF (graphic control, + // application, comment, plaintext, ...) comparteixen aquest layout. + inline void skipExtensionBlock(const uint8_t*& buffer) { + uint8_t block_size = *buffer++; + buffer += block_size; + uint8_t sub_block_size = *buffer++; + while (sub_block_size != 0) { + buffer += sub_block_size; + sub_block_size = *buffer++; + } + } + // Inicializa el diccionario LZW con los valores iniciales inline void initializeDictionary(std::vector& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming) int size = 1 << code_length; @@ -35,8 +62,8 @@ namespace GIF { } int bit = ((*input & mask) != 0) ? 1 : 0; mask <<= 1; - if (mask == 0x100) { - mask = 0x01; + if (mask == LZW_MASK_OVERFLOW) { + mask = LZW_MASK_INITIAL; input++; input_length--; } @@ -68,7 +95,10 @@ namespace GIF { dictionary[dictionary_ind].len = dictionary[prev].len + 1; dictionary_ind++; - if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < 11)) { + // Només podem créixer mentre no hagem arribat al màxim del decoder + // (12 bits); el `- 1` compensa el +1 que aplicarem tot seguit per + // a que code_length no sobrepasse LZW_MAX_CODE_LENGTH. + if ((dictionary_ind == (1 << (code_length + 1))) && (code_length < LZW_MAX_CODE_LENGTH - 1)) { code_length++; dictionary.resize(1 << (code_length + 1)); } @@ -92,14 +122,14 @@ namespace GIF { void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) { // Verifica que el code_length tenga un rango razonable. - if (code_length < 2 || code_length > 12) { + if (code_length < LZW_MIN_CODE_LENGTH || code_length > LZW_MAX_CODE_LENGTH) { throw std::runtime_error("Invalid LZW code length"); } int prev = -1; std::vector dictionary; int dictionary_ind; - unsigned int mask = 0x01; + unsigned int mask = LZW_MASK_INITIAL; int reset_code_length = code_length; int clear_code = 1 << code_length; int stop_code = clear_code + 1; @@ -123,7 +153,7 @@ namespace GIF { break; } - if (prev > -1 && code_length < 12) { + if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) { if (code > dictionary_ind) { std::cerr << "code = " << std::hex << code << ", but dictionary_ind = " << dictionary_ind << '\n'; @@ -159,7 +189,7 @@ namespace GIF { return data; } - auto Gif::processImageDescriptor(const uint8_t*& buffer, [[maybe_unused]] const std::vector& gct, [[maybe_unused]] int resolution_bits) -> std::vector { + auto Gif::processImageDescriptor(const uint8_t*& buffer) -> std::vector { ImageDescriptor image_descriptor; // Lee 9 bytes para el image descriptor. readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor)); @@ -185,15 +215,15 @@ namespace GIF { buffer += sizeof(ScreenDescriptor); std::vector global_color_table; - if ((screen_descriptor.fields & 0x80) != 0) { - int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1); + if ((screen_descriptor.fields & SCREEN_GCT_FLAG) != 0) { + int global_color_table_size = 1 << ((screen_descriptor.fields & SCREEN_GCT_SIZE_MASK) + 1); global_color_table.resize(global_color_table_size); for (int i = 0; i < global_color_table_size; ++i) { uint8_t r = buffer[0]; uint8_t g = buffer[1]; uint8_t b = buffer[2]; global_color_table[i] = (r << 16) | (g << 8) | b; - buffer += 3; + buffer += GCT_BYTES_PER_ENTRY; } } return global_color_table; @@ -205,79 +235,34 @@ namespace GIF { std::memcpy(header, buffer, 6); buffer += 6; - // Opcional: Validar header std::string header_str(reinterpret_cast(header), 6); if (header_str != "GIF87a" && header_str != "GIF89a") { throw std::runtime_error("Formato de archivo GIF inválido."); } - // Leer el Screen Descriptor (7 bytes, empaquetado sin padding) + // Screen Descriptor (7 bytes, empaquetado sin padding). ScreenDescriptor screen_descriptor; readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor)); - - // Asigna ancho y alto w = screen_descriptor.width; h = screen_descriptor.height; - int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1; - std::vector global_color_table; - if ((screen_descriptor.fields & 0x80) != 0) { - int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1); - global_color_table.resize(global_color_table_size); - std::memcpy(global_color_table.data(), buffer, static_cast(3) * static_cast(global_color_table_size)); - buffer += static_cast(3) * global_color_table_size; + // Saltar la Global Color Table si existeix; els colors els extrau + // loadPalette() en una passada separada sobre el mateix buffer. + if ((screen_descriptor.fields & SCREEN_GCT_FLAG) != 0) { + const int GCT_SIZE = 1 << ((screen_descriptor.fields & SCREEN_GCT_SIZE_MASK) + 1); + buffer += static_cast(GCT_BYTES_PER_ENTRY) * GCT_SIZE; } - // Supongamos que 'buffer' es el puntero actual y TRAILER es 0x3B + // Recorregut de blocs fins al TRAILER (0x3B). Les extensions comparteixen + // layout (mida + contingut fix + sub-blocs), per tant totes les saltem + // igual — no cal distingir per extension_label. uint8_t block_type = *buffer++; while (block_type != TRAILER) { - if (block_type == EXTENSION_INTRODUCER) // 0x21 - { - // Se lee la etiqueta de extensión, la cual indica el tipo de extensión. - uint8_t extension_label = *buffer++; - switch (extension_label) { - case GRAPHIC_CONTROL: // 0xF9 - { - // Procesar Graphic Control Extension: - uint8_t block_size = *buffer++; // Normalmente, blockSize == 4 - buffer += block_size; // Saltamos los 4 bytes del bloque fijo - // Saltar los sub-bloques - uint8_t sub_block_size = *buffer++; - while (sub_block_size != 0) { - buffer += sub_block_size; - sub_block_size = *buffer++; - } - break; - } - case APPLICATION_EXTENSION: // 0xFF - case COMMENT_EXTENSION: // 0xFE - case PLAINTEXT_EXTENSION: // 0x01 - { - // Para estas extensiones, saltamos el bloque fijo y los sub-bloques. - uint8_t block_size = *buffer++; - buffer += block_size; - uint8_t sub_block_size = *buffer++; - while (sub_block_size != 0) { - buffer += sub_block_size; - sub_block_size = *buffer++; - } - break; - } - default: { - // Si la etiqueta de extensión es desconocida, saltarla también: - uint8_t block_size = *buffer++; - buffer += block_size; - uint8_t sub_block_size = *buffer++; - while (sub_block_size != 0) { - buffer += sub_block_size; - sub_block_size = *buffer++; - } - break; - } - } + if (block_type == EXTENSION_INTRODUCER) { + ++buffer; // extension_label (ignorat) + skipExtensionBlock(buffer); } else if (block_type == IMAGE_DESCRIPTOR) { - // Procesar el Image Descriptor y retornar los datos de imagen - return processImageDescriptor(buffer, global_color_table, color_resolution_bits); + return processImageDescriptor(buffer); } else { std::cerr << "Unrecognized block type " << std::hex << static_cast(block_type) << '\n'; return std::vector{}; diff --git a/source/core/rendering/gif.hpp b/source/core/rendering/gif.hpp index c1e30a6..dc52f70 100644 --- a/source/core/rendering/gif.hpp +++ b/source/core/rendering/gif.hpp @@ -23,10 +23,6 @@ namespace GIF { uint8_t pixel_aspect_ratio; }; - struct RGB { - uint8_t r, g, b; - }; - struct ImageDescriptor { uint16_t image_left_position; uint16_t image_top_position; @@ -61,7 +57,7 @@ namespace GIF { static auto readSubBlocks(const uint8_t*& buffer) -> std::vector; // Procesa el Image Descriptor y retorna el vector de datos sin comprimir. - static auto processImageDescriptor(const uint8_t*& buffer, const std::vector& gct, int resolution_bits) -> std::vector; + static auto processImageDescriptor(const uint8_t*& buffer) -> std::vector; // Procesa el stream completo del GIF y devuelve los datos sin comprimir. static auto processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector; diff --git a/source/core/rendering/surface.cpp b/source/core/rendering/surface.cpp index 100c345..10768c9 100644 --- a/source/core/rendering/surface.cpp +++ b/source/core/rendering/surface.cpp @@ -37,13 +37,9 @@ Surface::Surface(int w, int h) } Surface::Surface(const std::string& file_path) { - SurfaceData loaded = loadSurface(file_path); - surface_data_ = std::make_shared(std::move(loaded)); - initializeSubPalette(sub_palette_); - palette_ = loadPaletteFromGif(file_path); -} - -auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { + // Carreguem el fitxer una única vegada i n'extraiem pixels i paleta de + // passades independents sobre el mateix buffer en memòria — abans es + // parsejava dues voltes (loadSurface + loadPaletteFromGif). auto buffer = Resource::Helper::loadFile(file_path); if (buffer.empty()) { throw std::runtime_error("Surface: cannot open " + file_path); @@ -58,9 +54,16 @@ auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { auto pixels = std::shared_ptr(new Uint8[raw_pixels.size()], std::default_delete()); std::memcpy(pixels.get(), raw_pixels.data(), raw_pixels.size()); + surface_data_ = std::make_shared(static_cast(w), static_cast(h), pixels); + initializeSubPalette(sub_palette_); + + std::vector rgb = GIF::Gif::loadPalette(buffer.data()); + const std::size_t COUNT = std::min(rgb.size(), palette_.size()); + for (std::size_t i = 0; i < COUNT; ++i) { + palette_[i] = 0xFF000000U | rgb[i]; + } printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]"); - return {static_cast(w), static_cast(h), pixels}; } void Surface::clear(Uint8 color) { diff --git a/source/core/rendering/surface.hpp b/source/core/rendering/surface.hpp index ee1aa3b..b721202 100644 --- a/source/core/rendering/surface.hpp +++ b/source/core/rendering/surface.hpp @@ -45,8 +45,6 @@ class Surface { explicit Surface(const std::string& file_path); ~Surface() = default; - static auto loadSurface(const std::string& file_path) -> SurfaceData; - // Vobla la regió indicada al framebuffer ARGB del joc usant la paleta propia. void render(int x, int y, const SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const; // Igual que render() però substituint els píxels amb valor source_color per From 8b413affa80344c9acdf69f496d87a319be089ed Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:55:40 +0200 Subject: [PATCH 14/19] refactor(core/rendering): Rendering::OPAQUE_ALPHA constant (HC-02) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface i Screen repetien el bit d'alpha opac com a 0xFF000000U o com (0xFFU << 24). S'afegeix inline constexpr Rendering::OPAQUE_ALPHA a canvas.hpp i s'usa a les tres ocurrències. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 5 +++-- source/core/rendering/canvas.hpp | 4 ++++ source/core/rendering/screen.cpp | 5 +++-- source/core/rendering/surface.cpp | 6 +++--- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 123d102..b694e83 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -43,7 +43,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| | HC-01 | `text.cpp:159` | `render(1, 15)` — `1` (tinta) y `15` (color por defecto) literales | Importante | DONE | `Text::INK_COLOR_INDEX` i `DEFAULT_TEXT_COLOR` | -| HC-02 | `surface.cpp:29`, `screen.cpp:56,218,244` | `0xFF000000U`/`0xFF` alfa repetido | Menor | PENDING | `OPAQUE_ALPHA` | +| HC-02 | `surface.cpp:29`, `screen.cpp:56,218,244` | `0xFF000000U`/`0xFF` alfa repetido | Menor | DONE | `Rendering::OPAQUE_ALPHA` a `canvas.hpp` | | HC-03 | `gif.cpp:71,95,…` | LZW `<11`/`<12`/`<2` y máscaras `0x80/0x70/0x07…` sin nombre | Menor | DONE | `LZW_MIN/MAX_CODE_LENGTH`, `SCREEN_GCT_FLAG`, `SCREEN_GCT_SIZE_MASK`, etc. | | HC-04 | `notifier.hpp:41-46`, `render_info.hpp:38-40` | HUD constants hardcoded en clase, no en `defaults`/YAML | Importante | PENDING | `Defaults::Hud::…` o config.yaml | | HC-05 | `render_info.cpp:19` + `notifier.hpp:43` | `TEXT_SIZE=8` duplicado | Menor | PENDING | constante global font | @@ -131,4 +131,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `597402a` — DEAD-04/05/07/08/10 + PATCH-04: neteja mort a `Sprite`/`AnimatedSprite`/`Surface` + `Sprite::update` inline - `ce3b6da` — DEAD-09: esborrats structs d'extensions GIF no usats - `bbb397a` — HC-06 + HC-07 + HC-09 + PATCH-08: constants nominals a `render_info` (segments, shadow offset, position count) i dedup init -- `` — DUP-01 + DUP-06 + HC-03 + HC-10 + PATCH-05: neteja parser GIF (extension single-skip, constants nominals LZW/GCT, signatura sense args morts, un sol parse per construcció de `Surface`) +- `be94a43` — DUP-01 + DUP-06 + HC-03 + HC-10 + PATCH-05: neteja parser GIF (extension single-skip, constants nominals LZW/GCT, signatura sense args morts, un sol parse per construcció de `Surface`) +- `` — HC-02: `Rendering::OPAQUE_ALPHA` a `canvas.hpp` reemplaça `0xFF000000U` repetit diff --git a/source/core/rendering/canvas.hpp b/source/core/rendering/canvas.hpp index 6e79a83..43ea158 100644 --- a/source/core/rendering/canvas.hpp +++ b/source/core/rendering/canvas.hpp @@ -10,6 +10,10 @@ // consumidors no depenguen del cicle de vida de Screen ni del seu header. namespace Rendering { + // Bit alpha completament opac, ja preposicionat al canal alt del + // píxel ARGB8888. Qualsevol RGB | OPAQUE_ALPHA produeix el píxel final. + inline constexpr std::uint32_t OPAQUE_ALPHA = 0xFF000000U; + struct Canvas { std::uint32_t* pixels{nullptr}; int width{0}; diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 6969554..a870b12 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -10,6 +10,7 @@ #include #include +#include "core/rendering/canvas.hpp" #include "utils/defines.hpp" namespace { @@ -52,7 +53,7 @@ void Screen::configure(const Config& config) { subsystem_initialized_ = true; canvas_width_ = GameCanvas::WIDTH; canvas_height_ = GameCanvas::HEIGHT; - canvas_.assign(static_cast(GameCanvas::WIDTH) * static_cast(GameCanvas::HEIGHT), 0xFF000000U); + canvas_.assign(static_cast(GameCanvas::WIDTH) * static_cast(GameCanvas::HEIGHT), Rendering::OPAQUE_ALPHA); } destroySdl(); config_ = config; @@ -214,7 +215,7 @@ void Screen::adjustWindowSize() { } 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); + const std::uint32_t COLOR = Rendering::OPAQUE_ALPHA | (static_cast(r) << 16) | (static_cast(g) << 8) | static_cast(b); std::ranges::fill(canvas_, COLOR); } diff --git a/source/core/rendering/surface.cpp b/source/core/rendering/surface.cpp index 10768c9..e1a7da1 100644 --- a/source/core/rendering/surface.cpp +++ b/source/core/rendering/surface.cpp @@ -23,10 +23,10 @@ auto loadPaletteFromGif(const std::string& file_path) -> Palette { } Palette palette = {}; - // Convertim RGB → ARGB amb canal alpha 0xFF. + // Convertim RGB → ARGB aplicant-hi el canal alpha opac constant. const std::size_t COUNT = std::min(rgb.size(), palette.size()); for (std::size_t i = 0; i < COUNT; ++i) { - palette[i] = 0xFF000000U | rgb[i]; + palette[i] = Rendering::OPAQUE_ALPHA | rgb[i]; } return palette; } @@ -60,7 +60,7 @@ Surface::Surface(const std::string& file_path) { std::vector rgb = GIF::Gif::loadPalette(buffer.data()); const std::size_t COUNT = std::min(rgb.size(), palette_.size()); for (std::size_t i = 0; i < COUNT; ++i) { - palette_[i] = 0xFF000000U | rgb[i]; + palette_[i] = Rendering::OPAQUE_ALPHA | rgb[i]; } printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]"); From da66a7c03fa0a50299a4d0217a074f6aa3922df2 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:57:17 +0200 Subject: [PATCH 15/19] refactor(core/rendering): netejar ctors de Sprite/MovingSprite (INC-06 + INC-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MovingSprite: el ctor delegat Sprite(surface, pos) ja assigna pos_; el Sprite::pos_ = pos; manual que seguia era redundant (INC-06). - Sprite(surface) usava initializer posicional {0.0F, 0.0F, ...} mentre la resta del projecte usa designated initializers ({.x = ..., .y = ...}). Unificat (INC-07). INC-05 (el static_cast de pos_.x a MovingSprite::move i setters) es marca WONTFIX al document: és snap intencional a píxel per al pixel-art, divergent només al setter per SDL_FRect que es reserva per a posicionament directe sense arrodoniment. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 9 +++++---- source/core/rendering/sprite/moving_sprite.cpp | 6 ++++-- source/core/rendering/sprite/sprite.cpp | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index b694e83..e0fc9d1 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -86,9 +86,9 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | INC-02 | `screen.cpp:52,104,125` vs `gif.cpp:96` vs `sdl3gpu_shader.cpp:214` | `exit`/throw/`bool` mezclados | Importante | DEFERRED | decisión global | | INC-03 | `screen.cpp`, `sdl3gpu_shader.cpp`, `surface.cpp`, `gif.cpp` | `fprintf`/`cerr`/`cout` mezclados | Menor | DEFERRED | logger inexistente | | INC-04 | `text.hpp:48-57` | API discreta + `writeDX(flags)` coexisten | Importante | PENDING | elegir una | -| INC-05 | `moving_sprite.cpp:39-40,58-72` | `pos_.x = static_cast(x_)` sobre `SDL_FRect` | Importante | PENDING | fijar semántica | -| INC-06 | `moving_sprite.cpp:12-21` | `Sprite::pos_ = pos;` redundante tras ctor delegado | Menor | PENDING | limpiar | -| INC-07 | `sprite.cpp:19-22` | initializer posicional vs designated | Menor | PENDING | uniformar | +| INC-05 | `moving_sprite.cpp:39-40,58-72` | `pos_.x = static_cast(x_)` sobre `SDL_FRect` | Importante | WONTFIX | És snap intencional a píxel per al pixel-art; setPos(SDL_FRect) es reserva per a posicionament directe sense arrodoniment. La divergència és una decisió, no un bug | +| INC-06 | `moving_sprite.cpp:12-21` | `Sprite::pos_ = pos;` redundante tras ctor delegado | Menor | DONE | línies eliminades | +| INC-07 | `sprite.cpp:19-22` | initializer posicional vs designated | Menor | DONE | designated initializer uniforme | ## 6. Código muerto @@ -132,4 +132,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `ce3b6da` — DEAD-09: esborrats structs d'extensions GIF no usats - `bbb397a` — HC-06 + HC-07 + HC-09 + PATCH-08: constants nominals a `render_info` (segments, shadow offset, position count) i dedup init - `be94a43` — DUP-01 + DUP-06 + HC-03 + HC-10 + PATCH-05: neteja parser GIF (extension single-skip, constants nominals LZW/GCT, signatura sense args morts, un sol parse per construcció de `Surface`) -- `` — HC-02: `Rendering::OPAQUE_ALPHA` a `canvas.hpp` reemplaça `0xFF000000U` repetit +- `8b413af` — HC-02: `Rendering::OPAQUE_ALPHA` a `canvas.hpp` reemplaça `0xFF000000U` repetit +- `` — INC-06 + INC-07: eliminats assignaments redundants `Sprite::pos_` a `MovingSprite`; designated initializer uniforme diff --git a/source/core/rendering/sprite/moving_sprite.cpp b/source/core/rendering/sprite/moving_sprite.cpp index b4cfeea..ee2074f 100644 --- a/source/core/rendering/sprite/moving_sprite.cpp +++ b/source/core/rendering/sprite/moving_sprite.cpp @@ -9,16 +9,18 @@ MovingSprite::MovingSprite() { Sprite::clear(); } MovingSprite::MovingSprite(std::shared_ptr surface) : Sprite(std::move(surface)) { Sprite::clear(); } +// pos_ ja queda inicialitzat pel ctor delegat Sprite(surface, pos); només +// cal sincronitzar la posició float x_/y_ (i el flip si ve). MovingSprite::MovingSprite(std::shared_ptr surface, SDL_FRect pos) : Sprite(std::move(surface), pos), x_(pos.x), - y_(pos.y) { Sprite::pos_ = pos; } + y_(pos.y) {} MovingSprite::MovingSprite(std::shared_ptr surface, SDL_FRect pos, SDL_FlipMode flip) : Sprite(std::move(surface), pos), x_(pos.x), y_(pos.y), - flip_(flip) { Sprite::pos_ = pos; } + flip_(flip) {} void MovingSprite::clear() { x_ = 0.0F; diff --git a/source/core/rendering/sprite/sprite.cpp b/source/core/rendering/sprite/sprite.cpp index 2e04939..3320fbb 100644 --- a/source/core/rendering/sprite/sprite.cpp +++ b/source/core/rendering/sprite/sprite.cpp @@ -18,7 +18,7 @@ Sprite::Sprite(std::shared_ptr surface, SDL_FRect rect) Sprite::Sprite(std::shared_ptr surface) : surface_(std::move(surface)), - pos_{0.0F, 0.0F, static_cast(surface_->getWidth()), static_cast(surface_->getHeight())}, + pos_{.x = 0.0F, .y = 0.0F, .w = static_cast(surface_->getWidth()), .h = static_cast(surface_->getHeight())}, clip_(pos_) {} void Sprite::render() { From 2350f4060f5a27ab0b9f34d15b81b5465b9810b6 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:58:14 +0200 Subject: [PATCH 16/19] refactor(core/rendering): nomenar magic numbers al shader CRT-Pi (HC-08) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crtpi_frag.msl tenia la línia float filter_width = (768.0f / u.texture_height) / 3.0f; amb dues constants soltes (768 i 3) sense explicar. Ara es declaren al mateix shader com CRT_REFERENCE_HEIGHT i SUBPIXEL_DIV amb un comentari explicant que 768 és l'alçada CRT de referència on es va tarar el filtre i 3 el nombre de subpíxels (R/G/B) del multisample. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 5 +++-- source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h | 8 +++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index e0fc9d1..de9f210 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -49,7 +49,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | HC-05 | `render_info.cpp:19` + `notifier.hpp:43` | `TEXT_SIZE=8` duplicado | Menor | PENDING | constante global font | | HC-06 | `render_info.cpp:64,138-139,193-195` | `segments_[3]` hardcode del segmento tiempo | Importante | DONE | constants `FPS/DRIVER/SHADER/TIME_SEGMENT` | | HC-07 | `render_info.cpp:207` | `% 3` hardcode nº de entradas enum | Menor | DONE | `Defaults::RENDER_INFO_POSITION_COUNT` | -| HC-08 | `crtpi_pipeline.cpp:97` | `768.0f / h / 3.0f` magic en shader | Menor | PENDING | uniforme o comentario | +| HC-08 | `crtpi_pipeline.cpp:97` | `768.0f / h / 3.0f` magic en shader | Menor | DONE | `CRT_REFERENCE_HEIGHT` / `SUBPIXEL_DIV` a l'MSL amb explicació | | HC-09 | `render_info.cpp:179-183,194-195` | shadow offset +1,+1 magic | Menor | DONE | `SHADOW_OFFSET_PX` | | HC-10 | `gif.cpp:162` | `processImageDescriptor` params `[[maybe_unused]]` vestigiales | Menor | DONE | signatura sense `gct` ni `resolution_bits`; `RGB` també esborrat | @@ -133,4 +133,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `bbb397a` — HC-06 + HC-07 + HC-09 + PATCH-08: constants nominals a `render_info` (segments, shadow offset, position count) i dedup init - `be94a43` — DUP-01 + DUP-06 + HC-03 + HC-10 + PATCH-05: neteja parser GIF (extension single-skip, constants nominals LZW/GCT, signatura sense args morts, un sol parse per construcció de `Surface`) - `8b413af` — HC-02: `Rendering::OPAQUE_ALPHA` a `canvas.hpp` reemplaça `0xFF000000U` repetit -- `` — INC-06 + INC-07: eliminats assignaments redundants `Sprite::pos_` a `MovingSprite`; designated initializer uniforme +- `da66a7c` — INC-06 + INC-07: eliminats assignaments redundants `Sprite::pos_` a `MovingSprite`; designated initializer uniforme +- `` — HC-08: constants nominals a l'MSL de CRT-Pi amb explicació del 768/3 diff --git a/source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h b/source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h index e770feb..f7989ea 100644 --- a/source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h +++ b/source/core/rendering/sdl3gpu/msl/crtpi_frag.msl.h @@ -67,7 +67,13 @@ fragment float4 crtpi_fs(PostVOut in [[stage_in]], sampler samp [[sampler(0)]], constant CrtPiUniforms& u [[buffer(0)]]) { float2 tex_size = float2(u.texture_width, u.texture_height); - float filter_width = (768.0f / u.texture_height) / 3.0f; + // Amplada del filtre de scanline analític. 768 = alçada de referència + // CRT a la qual es va tarar l'algoritme original; 3 = divisió per + // subpíxel (R/G/B) del multisample. El resultat escala amb la textura + // d'entrada, de manera que més alçada → filtre més fi. + const float CRT_REFERENCE_HEIGHT = 768.0f; + const float SUBPIXEL_DIV = 3.0f; + float filter_width = (CRT_REFERENCE_HEIGHT / u.texture_height) / SUBPIXEL_DIV; float2 texcoord = in.uv; if (u.enable_curvature != 0) { From e1e8bc11a5df6c8f4499c539d08d3cdf2696743a Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 09:59:30 +0200 Subject: [PATCH 17/19] refactor(core/rendering): unificar defaults de CRT-Pi (DUP-07) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Els defaults del shader CRT-Pi es duplicaven entre CrtPiParams (shader_backend.hpp) i CrtPiUniforms (crtpi_pipeline.hpp). Ara CrtPiUniforms es value-initializa a zeros i CrtPiPipeline::init(), un cop el pipeline està creat, aplica setParams(CrtPiParams{}) per a fixar els defaults canònics abans del primer render. Així queda una sola font de veritat (els defaults del struct CrtPiParams al interface ShaderBackend). Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 5 +++-- .../sdl3gpu/pipelines/crtpi_pipeline.cpp | 4 ++++ .../sdl3gpu/pipelines/crtpi_pipeline.hpp | 21 +++---------------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index de9f210..7ac97e1 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -36,7 +36,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | DUP-04 | `sdl3gpu_shader.cpp:165-201` + `crtpi_pipeline.cpp:165-202` | `createShaderMSL/SPIRV` duplicados | Importante | DONE | `sdl3gpu/shader_factory.hpp` | | DUP-05 | `sdl3gpu_shader.cpp:18-35` + `crtpi_pipeline.cpp:17-34` | vertex shader MSL duplicado | Menor | DONE | `sdl3gpu/msl/postfx_vert.msl.h` compartit | | DUP-06 | `surface.cpp:14-43` | constructor Surface abre y parsea GIF 2 veces | Importante | DONE | buffer carregat una volta, pixels+palette del mateix | -| DUP-07 | `crtpi_pipeline.hpp:70-87` + `shader_backend.hpp:30-45` + `crtpi_pipeline.cpp:45-62` | defaults CRT-Pi triples | Importante | PENDING | single source of truth | +| DUP-07 | `crtpi_pipeline.hpp:70-87` + `shader_backend.hpp:30-45` + `crtpi_pipeline.cpp:45-62` | defaults CRT-Pi triples | Importante | DONE | `CrtPiUniforms{}` value-init; init() aplica `CrtPiParams{}` com a única font | ## 2. Hardcoding @@ -134,4 +134,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `be94a43` — DUP-01 + DUP-06 + HC-03 + HC-10 + PATCH-05: neteja parser GIF (extension single-skip, constants nominals LZW/GCT, signatura sense args morts, un sol parse per construcció de `Surface`) - `8b413af` — HC-02: `Rendering::OPAQUE_ALPHA` a `canvas.hpp` reemplaça `0xFF000000U` repetit - `da66a7c` — INC-06 + INC-07: eliminats assignaments redundants `Sprite::pos_` a `MovingSprite`; designated initializer uniforme -- `` — HC-08: constants nominals a l'MSL de CRT-Pi amb explicació del 768/3 +- `2350f40` — HC-08: constants nominals a l'MSL de CRT-Pi amb explicació del 768/3 +- `` — DUP-07: `CrtPiParams{}` com a única font de veritat dels defaults de CRT-Pi diff --git a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp index 9aed88b..bb29ed2 100644 --- a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp +++ b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp @@ -60,6 +60,10 @@ namespace Rendering { std::cerr << "CrtPiPipeline: creació del pipeline ha fallat: " << SDL_GetError() << '\n'; return false; } + + // Fixem els defaults canònics abans del primer render; si Screen + // crida setParams més tard els sobreescriurà. + setParams(CrtPiParams{}); return true; } diff --git a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp index d94a5f6..99c45d9 100644 --- a/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp +++ b/source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.hpp @@ -67,24 +67,9 @@ namespace Rendering { SDL_GPUDevice* device_{nullptr}; SDL_GPUGraphicsPipeline* pipeline_{nullptr}; - CrtPiUniforms uniforms_{ - .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 = 1, - .enable_multisample = 1, - .enable_gamma = 1, - .enable_curvature = 0, - .enable_sharper = 0, - .texture_width = 0.0F, - .texture_height = 0.0F, - }; + // Inicialitzat amb value-init; init() el sobreescriu aplicant el + // CrtPiParams{} (defaults canònics, una sola font de veritat). + CrtPiUniforms uniforms_{}; }; } // namespace Rendering From f692739fed5da1e091f25d8b3ace732d484f6667 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 10:01:19 +0200 Subject: [PATCH 18/19] refactor(core/rendering): FONT_SIZE_PX compartit a hud_constants.hpp (HC-05) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notifier::TEXT_SIZE i el TEXT_SIZE del namespace anònim de render_info declaraven, els dos, "8" com l'alçada del glif de 8bithud. Ara Rendering::Hud::FONT_SIZE_PX (a core/rendering/hud_constants.hpp) és l'única font de veritat; Notifier i RenderInfo l'inclouen. HC-04 (moure la resta de constants HUD a YAML) es marca DEFERRED al document: és una feature de runtime-config, no pura neteja. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 7 ++++--- source/core/rendering/hud_constants.hpp | 13 +++++++++++++ source/core/rendering/notifier.hpp | 5 +++-- source/core/rendering/render_info.cpp | 3 ++- 4 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 source/core/rendering/hud_constants.hpp diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 7ac97e1..1d06465 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -45,8 +45,8 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | HC-01 | `text.cpp:159` | `render(1, 15)` — `1` (tinta) y `15` (color por defecto) literales | Importante | DONE | `Text::INK_COLOR_INDEX` i `DEFAULT_TEXT_COLOR` | | HC-02 | `surface.cpp:29`, `screen.cpp:56,218,244` | `0xFF000000U`/`0xFF` alfa repetido | Menor | DONE | `Rendering::OPAQUE_ALPHA` a `canvas.hpp` | | HC-03 | `gif.cpp:71,95,…` | LZW `<11`/`<12`/`<2` y máscaras `0x80/0x70/0x07…` sin nombre | Menor | DONE | `LZW_MIN/MAX_CODE_LENGTH`, `SCREEN_GCT_FLAG`, `SCREEN_GCT_SIZE_MASK`, etc. | -| HC-04 | `notifier.hpp:41-46`, `render_info.hpp:38-40` | HUD constants hardcoded en clase, no en `defaults`/YAML | Importante | PENDING | `Defaults::Hud::…` o config.yaml | -| HC-05 | `render_info.cpp:19` + `notifier.hpp:43` | `TEXT_SIZE=8` duplicado | Menor | PENDING | constante global font | +| HC-04 | `notifier.hpp:41-46`, `render_info.hpp:38-40` | HUD constants hardcoded en clase, no en `defaults`/YAML | Importante | DEFERRED | Moure a YAML és una feature; de moment consolidades a `hud_constants.hpp` (via HC-05) | +| HC-05 | `render_info.cpp:19` + `notifier.hpp:43` | `TEXT_SIZE=8` duplicado | Menor | DONE | `Rendering::Hud::FONT_SIZE_PX` a `hud_constants.hpp` | | HC-06 | `render_info.cpp:64,138-139,193-195` | `segments_[3]` hardcode del segmento tiempo | Importante | DONE | constants `FPS/DRIVER/SHADER/TIME_SEGMENT` | | HC-07 | `render_info.cpp:207` | `% 3` hardcode nº de entradas enum | Menor | DONE | `Defaults::RENDER_INFO_POSITION_COUNT` | | HC-08 | `crtpi_pipeline.cpp:97` | `768.0f / h / 3.0f` magic en shader | Menor | DONE | `CRT_REFERENCE_HEIGHT` / `SUBPIXEL_DIV` a l'MSL amb explicació | @@ -135,4 +135,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `8b413af` — HC-02: `Rendering::OPAQUE_ALPHA` a `canvas.hpp` reemplaça `0xFF000000U` repetit - `da66a7c` — INC-06 + INC-07: eliminats assignaments redundants `Sprite::pos_` a `MovingSprite`; designated initializer uniforme - `2350f40` — HC-08: constants nominals a l'MSL de CRT-Pi amb explicació del 768/3 -- `` — DUP-07: `CrtPiParams{}` com a única font de veritat dels defaults de CRT-Pi +- `e1e8bc1` — DUP-07: `CrtPiParams{}` com a única font de veritat dels defaults de CRT-Pi +- `` — HC-05: `Rendering::Hud::FONT_SIZE_PX` a `hud_constants.hpp` (consolida `TEXT_SIZE=8`) diff --git a/source/core/rendering/hud_constants.hpp b/source/core/rendering/hud_constants.hpp new file mode 100644 index 0000000..8fc4729 --- /dev/null +++ b/source/core/rendering/hud_constants.hpp @@ -0,0 +1,13 @@ +#pragma once + +// Constants compartides per als overlays HUD del projecte (Notifier, +// RenderInfo, ...). Són compile-time i depenen de la geometria del bitmap +// font 8bithud; en una fase futura es podran fer runtime llegint el .fnt, +// però de moment evitar duplicar-les ja és guany net. +namespace Rendering::Hud { + + // Alçada del glif de la font 8bithud en píxels del canvas. Coincideix + // amb box_height al .fnt i s'usa com a base de càlcul d'alçades HUD. + constexpr int FONT_SIZE_PX = 8; + +} // namespace Rendering::Hud diff --git a/source/core/rendering/notifier.hpp b/source/core/rendering/notifier.hpp index 0f4dc09..0960fde 100644 --- a/source/core/rendering/notifier.hpp +++ b/source/core/rendering/notifier.hpp @@ -6,6 +6,8 @@ #include #include +#include "core/rendering/hud_constants.hpp" + class Text; // Popup transitori a dalt i centrat. Entra lliscant des de dalt, es queda @@ -34,8 +36,7 @@ class Notifier { static constexpr int PADDING_H = 6; static constexpr int PADDING_V = 3; - static constexpr int TEXT_SIZE = 8; - static constexpr int HEIGHT = TEXT_SIZE + (2 * PADDING_V); + static constexpr int HEIGHT = Rendering::Hud::FONT_SIZE_PX + (2 * PADDING_V); static constexpr int MARGIN_TOP = 2; // Separació amb la vora superior del canvas. static constexpr float SLIDE_SPEED = 120.0F; diff --git a/source/core/rendering/render_info.cpp b/source/core/rendering/render_info.cpp index 738f39f..f460272 100644 --- a/source/core/rendering/render_info.cpp +++ b/source/core/rendering/render_info.cpp @@ -6,6 +6,7 @@ #include #include +#include "core/rendering/hud_constants.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/text.hpp" #include "core/resources/resource_cache.hpp" @@ -14,7 +15,7 @@ namespace { - constexpr int TEXT_SIZE = 8; + constexpr int TEXT_SIZE = Rendering::Hud::FONT_SIZE_PX; constexpr int PADDING_V = (RenderInfo::HEIGHT - TEXT_SIZE) / 2; // Llindar per saltar el dibuixat d'un segment quasi totalment transparent. From 32d60677526ad7539fd4e8ef0cc65560423648c5 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Mon, 20 Apr 2026 10:05:58 +0200 Subject: [PATCH 19/19] refactor(core/rendering): injectar font i paleta als overlays (COUP-04) + tancament auditoria MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notifier::init i RenderInfo::init cridaven Resource::Cache::get() i construïen cadascun la seua pròpia instància de Text a partir de "8bithud.gif" + "data/font/8bithud.fnt" (strings hardcoded). Notifier també llegia la paleta "ui" a cada render(). Ara: - Director construeix una sola std::shared_ptr compartida. - RenderInfo::init(Frame, shared_ptr) i Notifier::init (shared_ptr, const Palette&) reben els recursos injectats. - Notifier desa la Palette una volta i la consulta localment a render(), sense tocar el cache global. Resultat: els overlays perden la dependència dels noms dels assets i del cache; el cicle de vida queda controlat pel Director. El camp intern text_ passa a shared_ptr. El document d'auditoria es tanca: - PATCH-02, COUP-05 → DEFERRED (feature beyond cleanup scope). - INC-04 → WONTFIX (coexistència justificada de writeDX/write*). - Estat global: neteja completada. Build + smoke 5s xvfb OK (exit 124). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit-core-rendering.md | 13 +++++++------ source/core/rendering/notifier.cpp | 19 ++++++++----------- source/core/rendering/notifier.hpp | 8 ++++++-- source/core/rendering/render_info.cpp | 7 +++---- source/core/rendering/render_info.hpp | 7 +++++-- source/core/system/director.cpp | 12 ++++++++++-- 6 files changed, 39 insertions(+), 27 deletions(-) diff --git a/docs/audit-core-rendering.md b/docs/audit-core-rendering.md index 1d06465..de6cc42 100644 --- a/docs/audit-core-rendering.md +++ b/docs/audit-core-rendering.md @@ -1,7 +1,7 @@ # Auditoría `core/rendering/` Rama: `audit/core-rendering` -Estado global: en curso +Estado global: neteja completada. Restant: 3 ítems `DEFERRED` i 4 `WONTFIX` — tots documentats amb justificació. Estados posibles: - `PENDING` — identificado, sin tocar @@ -58,7 +58,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas | |----|---------------|----------|-----------|--------|-------| | PATCH-01 | `screen.cpp:71-92,210-214`, `sdl3gpu_shader.cpp:445-493` | workarounds Wayland/Vulkan | Importante | WONTFIX (de momento) | RAII para forzar orden — deferred | -| PATCH-02 | `render_info.cpp:85-88` | guard bug-fix animation | Menor | PENDING | refactor FSM animación | +| PATCH-02 | `render_info.cpp:85-88` | guard bug-fix animation | Menor | DEFERRED | El guard és ara la única línia "sense refactor" de l'animació; una màquina d'estats neta és feature, no cleanup | | PATCH-03 | `notifier.cpp:78-85` | TODO implícito de cola FIFO | Menor | WONTFIX | documentar decisión | | PATCH-04 | `sprite.cpp:47-49` | `Sprite::update` vacío hook no invocado | Menor | DONE | resolt amb DEAD-10 | | PATCH-05 | `gif.cpp:12-15` | comentario "reemplaçar macro READ" | Menor | DONE | comentari neutre nou | @@ -74,8 +74,8 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | COUP-01b | `render_info.cpp:12-13` | `core/rendering/render_info.cpp` incluye `game/options.hpp` + `game/ui/ui_palette.hpp` | Crítico | DONE | `RenderInfo::Frame` injectada; Options.buildRenderInfoFrame() | | COUP-02a | `screen.cpp:13-14,109` | `Screen` instancia `SDL3GPUShader` concreto | Importante | DONE | `Rendering::makeDefaultShaderBackend()` factory | | COUP-03 | `surface.cpp:81-83,110-112` | `Surface::render` usa `Screen::get()` global | Importante | DONE | `Rendering::activeCanvas()` intermediari | -| COUP-04 | `notifier.cpp:19`, `render_info.cpp:52` | `Resource::Cache::get().getSurface("8bithud.gif")` con string literal | Importante | PENDING | inyectar la surface | -| COUP-05 | `animated_sprite.cpp:9,95-116` | parser YAML incrustado en sprite | Importante | PENDING | `AnimationLoader` en `core/resources` | +| COUP-04 | `notifier.cpp:19`, `render_info.cpp:52` | `Resource::Cache::get().getSurface("8bithud.gif")` con string literal | Importante | DONE | Director construeix un `shared_ptr` i l'injecta a tots dos; Notifier també rep `Palette ui_palette` | +| COUP-05 | `animated_sprite.cpp:9,95-116` | parser YAML incrustado en sprite | Importante | DEFERRED | Requereix crear `AnimationLoader` a `core/resources` i reescriure el ctor; fora del scope de neteja d'aquesta auditoria | | COUP-06 | `notifier.hpp:22`, `render_info.hpp:17`, `screen.hpp:13` | 3 Meyers singletons en mismo subsistema | Importante | DEFERRED | requiere tocar Director | ## 5. Inconsistencias internas @@ -85,7 +85,7 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic | INC-01 | todo el subsistema | smart pointers mezclados (shared/unique/raw sin política) | Importante | DEFERRED | requiere decisión de política | | INC-02 | `screen.cpp:52,104,125` vs `gif.cpp:96` vs `sdl3gpu_shader.cpp:214` | `exit`/throw/`bool` mezclados | Importante | DEFERRED | decisión global | | INC-03 | `screen.cpp`, `sdl3gpu_shader.cpp`, `surface.cpp`, `gif.cpp` | `fprintf`/`cerr`/`cout` mezclados | Menor | DEFERRED | logger inexistente | -| INC-04 | `text.hpp:48-57` | API discreta + `writeDX(flags)` coexisten | Importante | PENDING | elegir una | +| INC-04 | `text.hpp:48-57` | API discreta + `writeDX(flags)` coexisten | Importante | WONTFIX | `writeDX` és un orquestrador prim sobre `write`/`writeColored`; Render_info usa la via directa, Notifier la via flags. Coexistència justificada, no doblegat. | | INC-05 | `moving_sprite.cpp:39-40,58-72` | `pos_.x = static_cast(x_)` sobre `SDL_FRect` | Importante | WONTFIX | És snap intencional a píxel per al pixel-art; setPos(SDL_FRect) es reserva per a posicionament directe sense arrodoniment. La divergència és una decisió, no un bug | | INC-06 | `moving_sprite.cpp:12-21` | `Sprite::pos_ = pos;` redundante tras ctor delegado | Menor | DONE | línies eliminades | | INC-07 | `sprite.cpp:19-22` | initializer posicional vs designated | Menor | DONE | designated initializer uniforme | @@ -136,4 +136,5 @@ Los IDs se mantienen estables una vez asignados. Cada entrada apunta al(los) fic - `da66a7c` — INC-06 + INC-07: eliminats assignaments redundants `Sprite::pos_` a `MovingSprite`; designated initializer uniforme - `2350f40` — HC-08: constants nominals a l'MSL de CRT-Pi amb explicació del 768/3 - `e1e8bc1` — DUP-07: `CrtPiParams{}` com a única font de veritat dels defaults de CRT-Pi -- `` — HC-05: `Rendering::Hud::FONT_SIZE_PX` a `hud_constants.hpp` (consolida `TEXT_SIZE=8`) +- `f692739` — HC-05: `Rendering::Hud::FONT_SIZE_PX` a `hud_constants.hpp` (consolida `TEXT_SIZE=8`) +- `` — COUP-04 + tancament: injecció de `shared_ptr` i `Palette` als overlays des del Director; estat final de l'auditoria documentat diff --git a/source/core/rendering/notifier.cpp b/source/core/rendering/notifier.cpp index 1c59600..33e3f4d 100644 --- a/source/core/rendering/notifier.cpp +++ b/source/core/rendering/notifier.cpp @@ -1,9 +1,9 @@ #include "core/rendering/notifier.hpp" +#include + #include "core/rendering/screen.hpp" -#include "core/rendering/surface.hpp" #include "core/rendering/text.hpp" -#include "core/resources/resource_cache.hpp" #include "utils/defines.hpp" auto Notifier::get() -> Notifier& { @@ -13,11 +13,9 @@ auto Notifier::get() -> Notifier& { Notifier::~Notifier() = default; -void Notifier::init() { - // El Director preloade la paleta "ui" i la surface "8bithud.gif" abans - // de cridar aquest init(); ací només cal construir el Text. - auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); - text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); +void Notifier::init(std::shared_ptr text, const Palette& ui_palette) { + text_ = std::move(text); + ui_palette_ = ui_palette; } void Notifier::update(int delta_ms) { @@ -54,10 +52,9 @@ void Notifier::render() { const int BOX_X = (GameCanvas::WIDTH - BOX_W) / 2; const int BOX_Y = static_cast(y_); - // Fons sòlid a través de la paleta "ui" (ARGB directe al canvas, sense - // tocar cap surface paletada). - const auto& ui_palette = Resource::Cache::get().getPalette("ui"); - Screen::get().fillRect(BOX_X, BOX_Y, BOX_W, HEIGHT, ui_palette[current_bg_color_]); + // Fons sòlid amb la paleta UI que ens ha injectat el caller a init() + // (ARGB directe al canvas, sense tocar cap surface paletada). + Screen::get().fillRect(BOX_X, BOX_Y, BOX_W, HEIGHT, ui_palette_[current_bg_color_]); text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, diff --git a/source/core/rendering/notifier.hpp b/source/core/rendering/notifier.hpp index 0960fde..42ca726 100644 --- a/source/core/rendering/notifier.hpp +++ b/source/core/rendering/notifier.hpp @@ -7,6 +7,7 @@ #include #include "core/rendering/hud_constants.hpp" +#include "core/rendering/surface.hpp" class Text; @@ -27,7 +28,9 @@ class Notifier { auto operator=(const Notifier&) -> Notifier& = delete; auto operator=(Notifier&&) -> Notifier& = delete; - void init(); + // Injecta la font compartida i la paleta UI que usa la caixa de fons. + // Així Notifier no depèn de cap string de recurs ni del cache global. + void init(std::shared_ptr text, const Palette& ui_palette); void update(int delta_ms); void render(); @@ -49,7 +52,8 @@ class Notifier { Notifier() = default; ~Notifier(); - std::unique_ptr text_; + std::shared_ptr text_; + Palette ui_palette_{}; Status status_{Status::HIDDEN}; float y_{static_cast(-HEIGHT - MARGIN_TOP)}; std::string current_text_; diff --git a/source/core/rendering/render_info.cpp b/source/core/rendering/render_info.cpp index f460272..7d9044a 100644 --- a/source/core/rendering/render_info.cpp +++ b/source/core/rendering/render_info.cpp @@ -5,11 +5,11 @@ #include #include #include +#include #include "core/rendering/hud_constants.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/text.hpp" -#include "core/resources/resource_cache.hpp" #include "utils/defines.hpp" #include "utils/easing.hpp" @@ -57,9 +57,8 @@ auto RenderInfo::get() -> RenderInfo& { RenderInfo::~RenderInfo() = default; -void RenderInfo::init(const Frame& initial) { - auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); - text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); +void RenderInfo::init(const Frame& initial, std::shared_ptr text) { + text_ = std::move(text); current_ = initial; diff --git a/source/core/rendering/render_info.hpp b/source/core/rendering/render_info.hpp index 21bbd6f..50cdbdf 100644 --- a/source/core/rendering/render_info.hpp +++ b/source/core/rendering/render_info.hpp @@ -38,7 +38,10 @@ class RenderInfo { Uint8 shadow_color{0}; }; - void init(const Frame& initial); + // Injecta la font compartida a init(). La resta de l'estat visual ve + // pel Frame a cada frame, així que RenderInfo no depèn de cap recurs + // global. + void init(const Frame& initial, std::shared_ptr text); void update(int delta_ms, const Frame& frame); void render(); @@ -62,7 +65,7 @@ class RenderInfo { void refreshSegments(); [[nodiscard]] auto segmentWidth(const Segment& seg, int digit_cell_w) const -> int; - std::unique_ptr text_; + std::shared_ptr text_; Defaults::RenderInfoPosition visible_pos_{Defaults::RenderInfoPosition::OFF}; float info_anim_{0.0F}; // 0..1, suavitzat amb outQuad al render std::array segments_{}; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index a18d25b..89b6190 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -17,6 +17,7 @@ #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/surface.hpp" +#include "core/rendering/text.hpp" #include "core/resources/resource_cache.hpp" #include "core/scene/scene.hpp" #include "core/system/event_buffer.hpp" @@ -80,8 +81,15 @@ Director::Director() { // JA_Music_t que continga, així que no queden streams orfes. Audio::init(Options::buildAudioConfig()); - RenderInfo::get().init(Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW)); - Notifier::get().init(); + // Font compartida pels overlays: construïm una sola instància de Text + // i la injectem als consumidors (Notifier, RenderInfo) per a que cap + // d'ells hagi de conèixer els noms dels recursos concrets. + auto hud_font_surface = Resource::Cache::get().getSurface("8bithud.gif"); + auto hud_text = std::make_shared(hud_font_surface, "data/font/8bithud.fnt"); + const auto& ui_palette = Resource::Cache::get().getPalette("ui"); + + RenderInfo::get().init(Options::buildRenderInfoFrame(UiColor::RENDER_INFO_TEXT, UiColor::RENDER_INFO_SHADOW), hud_text); + Notifier::get().init(hud_text, ui_palette); current_scene_ = std::make_unique(); last_ticks_ms_ = SDL_GetTicks();