Auditoria de core/rendering: neteja general aplicada (rama audit/core-rendering)

Recull de 19 commits d'auditoria + 1 doc amb les troballes i el seu
estat. Veure docs/audit-core-rendering.md per al detall per-finding.

Resum per categoria:
- Duplicació: 7 items (surface blit, shaders MSL + factory, Text loops,
  Text glyph resolution, GIF extension switch, GIF double-parse, CRT-Pi
  defaults).
- Hardcoding: 9 items amb nom (INK_COLOR_INDEX, DEFAULT_TEXT_COLOR,
  OPAQUE_ALPHA, LZW/GCT constants, segment indices, position count,
  shadow offset, FONT_SIZE_PX, CRT reference height/subpixel div).
- Parxes: 5 neteja (readBytes comment, Sprite::update inline,
  mono_digits dedup init, GIF extension labels, NOLINTs eliminats).
- Acoblament: 4 desacoblaments (Notifier→game/ui; RenderInfo→Options;
  Screen→SDL3GPUShader concret; Surface→Screen; font+palette injectats).
- Codi mort: 9 items (writeShadowed/writeCentered/glyphWidth/
  getGlyphClip/codepointToUtf8/getSprite/getRect/incX/incY/
  getCurrentAnimationSize/getSurfaceData/Sprite::update/GIF extension
  structs).
- Convencions: shaders MSL a fitxers dedicats amb naming conforme.

Build verificat a cada commit; smoke test de 5s sota xvfb-run a cada
finding (exit 124 = corregut sencer, sense crash).

Items restants DEFERRED (no es tanquen com a cleanup perquè són
features o decisions d'arquitectura global): HC-04 (YAML HUD config),
PATCH-01 (RAII Wayland), PATCH-02 (FSM RenderInfo), COUP-05
(AnimationLoader), COUP-06 (reemplaçar singletons), INC-01/-02/-03
(polítiques globals de smart-pointers, errors i logging).
This commit is contained in:
JailDesigner committed 2026-04-20 10:24:20 +02:00
commit 0a3ef447ea
36 files changed
+875 -777

No files matched your search

+2
View File
@@ -39,6 +39,7 @@ set(APP_SOURCES
source/core/input/key_config.cpp source/core/input/key_config.cpp
source/core/input/global_inputs.cpp source/core/input/global_inputs.cpp
source/core/rendering/screen.cpp source/core/rendering/screen.cpp
source/core/rendering/canvas.cpp
source/core/rendering/notifier.cpp source/core/rendering/notifier.cpp
source/core/rendering/render_info.cpp source/core/rendering/render_info.cpp
source/core/rendering/gif.cpp source/core/rendering/gif.cpp
@@ -207,6 +208,7 @@ if(CPPCHECK_EXE)
--suppress=toomanyconfigs --suppress=toomanyconfigs
--suppress=*:*source/external/* --suppress=*:*source/external/*
--suppress=*:*source/core/rendering/sdl3gpu/spv/* --suppress=*:*source/core/rendering/sdl3gpu/spv/*
--suppress=*:*source/core/rendering/sdl3gpu/msl/*
--suppress=normalCheckLevelMaxBranches --suppress=normalCheckLevelMaxBranches
-D_DEBUG -D_DEBUG
-DLINUX_BUILD -DLINUX_BUILD
+140
View File
@@ -0,0 +1,140 @@
# Auditoría `core/rendering/`
Rama: `audit/core-rendering`
Estado global: neteja completada. Restant: 3 ítems `DEFERRED` i 4 `WONTFIX` — tots documentats amb justificació.
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/*` | 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 | DONE |
| COUP-02 | `Screen` instancia `SDL3GPUShader` directamente + `Surface` → `Screen::get()` | DONE |
---
## 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 | 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 | 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 | DONE | `CrtPiUniforms{}` value-init; init() aplica `CrtPiParams{}` com a única font |
## 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 | 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 | 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ó |
| 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 |
## 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 | 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 |
| 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 |
## 4. Acoplamiento innecesario
| 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 | 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 | DONE | Director construeix un `shared_ptr<Text>` 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
| 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 | 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<int>(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
| ID | Fichero:línea | Hallazgo | Severidad | Estado | Notas |
|----|---------------|----------|-----------|--------|-------|
| 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 | 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 | 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
| 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 | DONE |
| CONV-04 | Singletons vs "interfaces + callbacks" | COUP-06 | DEFERRED |
| CONV-05 | `core → game` deps | COUP-01 | cubierto por COUP-01 |
---
## Registro de commits
- `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`
- `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`
- `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`
- `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
- `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
- `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
- `f692739` — HC-05: `Rendering::Hud::FONT_SIZE_PX` a `hud_constants.hpp` (consolida `TEXT_SIZE=8`)
- `<pendent>` — COUP-04 + tancament: injecció de `shared_ptr<Text>` i `Palette` als overlays des del Director; estat final de l'auditoria documentat
+12 -8
View File
@@ -12,6 +12,7 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/ui/notification_presets.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
namespace GlobalInputs { namespace GlobalInputs {
@@ -37,7 +38,7 @@ namespace GlobalInputs {
} }
void notifyLocale(const std::string& key) { void notifyLocale(const std::string& key) {
Notifier::get().notify(Locale::get().text(key)); Notification::notify(Locale::get().text(key));
} }
void onDecZoom() { void onDecZoom() {
@@ -46,7 +47,7 @@ namespace GlobalInputs {
Options::window.zoom--; Options::window.zoom--;
Screen::get().setWindowZoom(Options::window.zoom); Screen::get().setWindowZoom(Options::window.zoom);
Options::saveToFile(); 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() { void onIncZoom() {
@@ -55,7 +56,7 @@ namespace GlobalInputs {
Options::window.zoom++; Options::window.zoom++;
Screen::get().setWindowZoom(Options::window.zoom); Screen::get().setWindowZoom(Options::window.zoom);
Options::saveToFile(); 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() { void onFullscreen() {
@@ -83,7 +84,7 @@ namespace GlobalInputs {
Options::video.shader.current_shader = (Options::video.shader.current_shader == "crtpi") ? "postfx" : "crtpi"; Options::video.shader.current_shader = (Options::video.shader.current_shader == "crtpi") ? "postfx" : "crtpi";
reapplyShader(); reapplyShader();
Options::saveToFile(); 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 // Busca l'entrada actual al vector de presets i passa a la següent
@@ -108,7 +109,7 @@ namespace GlobalInputs {
reapplyShader(); reapplyShader();
Options::saveToFile(); Options::saveToFile();
const std::string& name = IS_CRTPI ? Options::video.shader.current_crtpi_preset : Options::video.shader.current_postfx_preset; 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() { void onNextScalingMode() {
@@ -138,11 +139,14 @@ namespace GlobalInputs {
} }
void onCycleRenderInfo() { void onCycleRenderInfo() {
RenderInfo::cyclePosition(); const int NEXT = (static_cast<int>(Options::video.render_info_position) + 1) % Defaults::RENDER_INFO_POSITION_COUNT;
Options::video.render_info_position = static_cast<Defaults::RenderInfoPosition>(NEXT);
Options::saveToFile();
} }
void onToggleRenderTime() { void onToggleRenderTime() {
RenderInfo::toggleShowTime(); Options::video.render_info_show_time = !Options::video.render_info_show_time;
Options::saveToFile();
} }
void onExit() { void onExit() {
@@ -151,7 +155,7 @@ namespace GlobalInputs {
return; return;
} }
exit_window_s = EXIT_WINDOW_S; 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 } // namespace
+16
View File
@@ -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
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
// 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 {
// 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};
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
+59 -74
View File
@@ -1,19 +1,46 @@
#include "core/rendering/gif.hpp" #include "core/rendering/gif.hpp"
#include <cstring> // Para memcpy, size_t #include <cstring>
#include <iostream> // Para std::cout #include <iostream>
#include <stdexcept> // Para runtime_error #include <stdexcept>
#include <string> // Para allocator, char_traits, operator==, basic_string #include <string>
namespace GIF { namespace GIF {
// Función inline para reemplazar el macro READ. namespace {
// Actualiza el puntero 'buffer' tras copiar 'size' bytes a 'dst'. // 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) { inline void readBytes(const uint8_t*& buffer, void* dst, size_t size) {
std::memcpy(dst, buffer, size); std::memcpy(dst, buffer, size);
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 // Inicializa el diccionario LZW con los valores iniciales
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming) inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
int size = 1 << code_length; int size = 1 << code_length;
@@ -35,8 +62,8 @@ namespace GIF {
} }
int bit = ((*input & mask) != 0) ? 1 : 0; int bit = ((*input & mask) != 0) ? 1 : 0;
mask <<= 1; mask <<= 1;
if (mask == 0x100) { if (mask == LZW_MASK_OVERFLOW) {
mask = 0x01; mask = LZW_MASK_INITIAL;
input++; input++;
input_length--; input_length--;
} }
@@ -68,7 +95,10 @@ namespace GIF {
dictionary[dictionary_ind].len = dictionary[prev].len + 1; dictionary[dictionary_ind].len = dictionary[prev].len + 1;
dictionary_ind++; 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++; code_length++;
dictionary.resize(1 << (code_length + 1)); 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) { 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. // 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"); throw std::runtime_error("Invalid LZW code length");
} }
int prev = -1; int prev = -1;
std::vector<DictionaryEntry> dictionary; std::vector<DictionaryEntry> dictionary;
int dictionary_ind; int dictionary_ind;
unsigned int mask = 0x01; unsigned int mask = LZW_MASK_INITIAL;
int reset_code_length = code_length; int reset_code_length = code_length;
int clear_code = 1 << code_length; int clear_code = 1 << code_length;
int stop_code = clear_code + 1; int stop_code = clear_code + 1;
@@ -123,7 +153,7 @@ namespace GIF {
break; break;
} }
if (prev > -1 && code_length < 12) { if (prev > -1 && code_length < LZW_MAX_CODE_LENGTH) {
if (code > dictionary_ind) { if (code > dictionary_ind) {
std::cerr << "code = " << std::hex << code std::cerr << "code = " << std::hex << code
<< ", but dictionary_ind = " << dictionary_ind << '\n'; << ", but dictionary_ind = " << dictionary_ind << '\n';
@@ -159,7 +189,7 @@ namespace GIF {
return data; return data;
} }
auto Gif::processImageDescriptor(const uint8_t*& buffer, [[maybe_unused]] const std::vector<RGB>& gct, [[maybe_unused]] int resolution_bits) -> std::vector<uint8_t> { auto Gif::processImageDescriptor(const uint8_t*& buffer) -> std::vector<uint8_t> {
ImageDescriptor image_descriptor; ImageDescriptor image_descriptor;
// Lee 9 bytes para el image descriptor. // Lee 9 bytes para el image descriptor.
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor)); readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
@@ -185,15 +215,15 @@ namespace GIF {
buffer += sizeof(ScreenDescriptor); buffer += sizeof(ScreenDescriptor);
std::vector<uint32_t> global_color_table; std::vector<uint32_t> global_color_table;
if ((screen_descriptor.fields & 0x80) != 0) { if ((screen_descriptor.fields & SCREEN_GCT_FLAG) != 0) {
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1); int global_color_table_size = 1 << ((screen_descriptor.fields & SCREEN_GCT_SIZE_MASK) + 1);
global_color_table.resize(global_color_table_size); global_color_table.resize(global_color_table_size);
for (int i = 0; i < global_color_table_size; ++i) { for (int i = 0; i < global_color_table_size; ++i) {
uint8_t r = buffer[0]; uint8_t r = buffer[0];
uint8_t g = buffer[1]; uint8_t g = buffer[1];
uint8_t b = buffer[2]; uint8_t b = buffer[2];
global_color_table[i] = (r << 16) | (g << 8) | b; global_color_table[i] = (r << 16) | (g << 8) | b;
buffer += 3; buffer += GCT_BYTES_PER_ENTRY;
} }
} }
return global_color_table; return global_color_table;
@@ -205,79 +235,34 @@ namespace GIF {
std::memcpy(header, buffer, 6); std::memcpy(header, buffer, 6);
buffer += 6; buffer += 6;
// Opcional: Validar header
std::string header_str(reinterpret_cast<char*>(header), 6); std::string header_str(reinterpret_cast<char*>(header), 6);
if (header_str != "GIF87a" && header_str != "GIF89a") { if (header_str != "GIF87a" && header_str != "GIF89a") {
throw std::runtime_error("Formato de archivo GIF inválido."); 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; ScreenDescriptor screen_descriptor;
readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor)); readBytes(buffer, &screen_descriptor, sizeof(ScreenDescriptor));
// Asigna ancho y alto
w = screen_descriptor.width; w = screen_descriptor.width;
h = screen_descriptor.height; h = screen_descriptor.height;
int color_resolution_bits = ((screen_descriptor.fields & 0x70) >> 4) + 1; // Saltar la Global Color Table si existeix; els colors els extrau
std::vector<RGB> global_color_table; // loadPalette() en una passada separada sobre el mateix buffer.
if ((screen_descriptor.fields & 0x80) != 0) { if ((screen_descriptor.fields & SCREEN_GCT_FLAG) != 0) {
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1); const int GCT_SIZE = 1 << ((screen_descriptor.fields & SCREEN_GCT_SIZE_MASK) + 1);
global_color_table.resize(global_color_table_size); buffer += static_cast<ptrdiff_t>(GCT_BYTES_PER_ENTRY) * GCT_SIZE;
std::memcpy(global_color_table.data(), buffer, static_cast<size_t>(3) * static_cast<size_t>(global_color_table_size));
buffer += static_cast<ptrdiff_t>(3) * global_color_table_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++; uint8_t block_type = *buffer++;
while (block_type != TRAILER) { while (block_type != TRAILER) {
if (block_type == EXTENSION_INTRODUCER) // 0x21 if (block_type == EXTENSION_INTRODUCER) {
{ ++buffer; // extension_label (ignorat)
// Se lee la etiqueta de extensión, la cual indica el tipo de extensión. skipExtensionBlock(buffer);
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;
}
}
} else if (block_type == IMAGE_DESCRIPTOR) { } else if (block_type == IMAGE_DESCRIPTOR) {
// Procesar el Image Descriptor y retornar los datos de imagen return processImageDescriptor(buffer);
return processImageDescriptor(buffer, global_color_table, color_resolution_bits);
} else { } else {
std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n'; std::cerr << "Unrecognized block type " << std::hex << static_cast<int>(block_type) << '\n';
return std::vector<uint8_t>{}; return std::vector<uint8_t>{};
+1 -27
View File
@@ -23,10 +23,6 @@ namespace GIF {
uint8_t pixel_aspect_ratio; uint8_t pixel_aspect_ratio;
}; };
struct RGB {
uint8_t r, g, b;
};
struct ImageDescriptor { struct ImageDescriptor {
uint16_t image_left_position; uint16_t image_left_position;
uint16_t image_top_position; uint16_t image_top_position;
@@ -42,28 +38,6 @@ namespace GIF {
int len; 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 { class Gif {
public: public:
// Descompone (uncompress) el bloque comprimido usando LZW. // Descompone (uncompress) el bloque comprimido usando LZW.
@@ -83,7 +57,7 @@ namespace GIF {
static auto readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t>; static auto readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t>;
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir. // Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
static auto processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t>; static auto processImageDescriptor(const uint8_t*& buffer) -> std::vector<uint8_t>;
// Procesa el stream completo del GIF y devuelve los datos sin comprimir. // 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<uint8_t>; static auto processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t>;
+13
View File
@@ -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
+8 -19
View File
@@ -1,9 +1,9 @@
#include "core/rendering/notifier.hpp" #include "core/rendering/notifier.hpp"
#include <utility>
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/resources/resource_cache.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
auto Notifier::get() -> Notifier& { auto Notifier::get() -> Notifier& {
@@ -13,11 +13,9 @@ auto Notifier::get() -> Notifier& {
Notifier::~Notifier() = default; Notifier::~Notifier() = default;
void Notifier::init() { void Notifier::init(std::shared_ptr<Text> text, const Palette& ui_palette) {
// El Director preloade la paleta "ui" i la surface "8bithud.gif" abans text_ = std::move(text);
// de cridar aquest init(); ací només cal construir el Text. ui_palette_ = ui_palette;
auto font_surface = Resource::Cache::get().getSurface("8bithud.gif");
text_ = std::make_unique<Text>(font_surface, "data/font/8bithud.fnt");
} }
void Notifier::update(int delta_ms) { 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_X = (GameCanvas::WIDTH - BOX_W) / 2;
const int BOX_Y = static_cast<int>(y_); const int BOX_Y = static_cast<int>(y_);
// Fons sòlid a través de la paleta "ui" (ARGB directe al canvas, sense // Fons sòlid amb la paleta UI que ens ha injectat el caller a init()
// tocar cap surface paletada). // (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_]);
Screen::get().fillRect(BOX_X, BOX_Y, BOX_W, HEIGHT, ui_palette[current_bg_color_]);
text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
GameCanvas::CENTER_X, GameCanvas::CENTER_X,
@@ -67,14 +64,6 @@ void Notifier::render() {
current_text_color_); 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) { 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 // Política simple: la nova notificació reemplaça la que hi havia. Per
// gestionar-ne una cua, canviar ací per una FIFO quan calga. // gestionar-ne una cua, canviar ací per una FIFO quan calga.
+11 -12
View File
@@ -6,7 +6,8 @@
#include <memory> #include <memory>
#include <string> #include <string>
#include "game/ui/notification_presets.hpp" #include "core/rendering/hud_constants.hpp"
#include "core/rendering/surface.hpp"
class Text; class Text;
@@ -15,10 +16,9 @@ class Text;
// 8bithud i la paleta "ui" amb la resta d'overlays (carregades per RenderInfo // 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). // a l'arrencada; si Notifier::init() es crida primer, les carrega ell).
// //
// Ús típic: // API crua: Notifier::get().notify(text, text_color, bg_color, time_ms).
// Notifier::get().notify("Text ràpid"); // preset DEFAULT // Per a treballar amb presets del joc, vegeu Notification::notify() a
// Notifier::get().notify("Text ràpid", miPreset); // preset custom // game/ui/notification_presets.hpp.
// Notifier::get().notify("Urgent", 11, 3, 800); // colors i durada explícits
class Notifier { class Notifier {
public: public:
static auto get() -> Notifier&; static auto get() -> Notifier&;
@@ -28,20 +28,18 @@ class Notifier {
auto operator=(const Notifier&) -> Notifier& = delete; auto operator=(const Notifier&) -> Notifier& = delete;
auto operator=(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> text, const Palette& ui_palette);
void update(int delta_ms); void update(int delta_ms);
void render(); 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); void notify(const std::string& text, Uint8 text_color, Uint8 bg_color, int time_ms);
static constexpr int PADDING_H = 6; static constexpr int PADDING_H = 6;
static constexpr int PADDING_V = 3; static constexpr int PADDING_V = 3;
static constexpr int TEXT_SIZE = 8; static constexpr int HEIGHT = Rendering::Hud::FONT_SIZE_PX + (2 * PADDING_V);
static constexpr int HEIGHT = TEXT_SIZE + (2 * PADDING_V);
static constexpr int MARGIN_TOP = 2; // Separació amb la vora superior del canvas. static constexpr int MARGIN_TOP = 2; // Separació amb la vora superior del canvas.
static constexpr float SLIDE_SPEED = 120.0F; static constexpr float SLIDE_SPEED = 120.0F;
@@ -54,7 +52,8 @@ class Notifier {
Notifier() = default; Notifier() = default;
~Notifier(); ~Notifier();
std::unique_ptr<Text> text_; std::shared_ptr<Text> text_;
Palette ui_palette_{};
Status status_{Status::HIDDEN}; Status status_{Status::HIDDEN};
float y_{static_cast<float>(-HEIGHT - MARGIN_TOP)}; float y_{static_cast<float>(-HEIGHT - MARGIN_TOP)};
std::string current_text_; std::string current_text_;
+51 -59
View File
@@ -5,24 +5,33 @@
#include <cctype> #include <cctype>
#include <cstdio> #include <cstdio>
#include <string> #include <string>
#include <utility>
#include "core/rendering/hud_constants.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.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/defines.hpp"
#include "utils/easing.hpp" #include "utils/easing.hpp"
namespace { namespace {
constexpr int TEXT_SIZE = 8; constexpr int TEXT_SIZE = Rendering::Hud::FONT_SIZE_PX;
constexpr int PADDING_V = (RenderInfo::HEIGHT - TEXT_SIZE) / 2; constexpr int PADDING_V = (RenderInfo::HEIGHT - TEXT_SIZE) / 2;
// Llindar per saltar el dibuixat d'un segment quasi totalment transparent. // Llindar per saltar el dibuixat d'un segment quasi totalment transparent.
// Evita una fileta d'un sol píxel quan acaba de començar a aparèixer. // Evita una fileta d'un sol píxel quan acaba de començar a aparèixer.
constexpr float VISIBLE_EPSILON = 0.01F; constexpr float VISIBLE_EPSILON = 0.01F;
// 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 { auto prettify(std::string s) -> std::string {
std::ranges::replace(s, '-', ' '); std::ranges::replace(s, '-', ' ');
std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); }); std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); });
@@ -48,27 +57,25 @@ auto RenderInfo::get() -> RenderInfo& {
RenderInfo::~RenderInfo() = default; RenderInfo::~RenderInfo() = default;
void RenderInfo::init() { void RenderInfo::init(const Frame& initial, std::shared_ptr<Text> text) {
auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); text_ = std::move(text);
text_ = std::make_unique<Text>(font_surface, "data/font/8bithud.fnt");
current_ = initial;
// Restaura l'estat guardat: si l'overlay estava engegat, l'activem ja // 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
if (Options::video.render_info_position != Defaults::RenderInfoPosition::OFF) { // és qui marca mono_digits al segment de temps; si no s'engega ara,
visible_pos_ = Options::video.render_info_position; // 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; 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(); refreshSegments();
for (auto& seg : segments_) { seg.anim = seg.visible ? 1.0F : 0.0F; } 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; elapsed_ms_ += delta_ms;
const float DT = static_cast<float>(delta_ms) / 1000.0F; const float DT = static_cast<float>(delta_ms) / 1000.0F;
@@ -76,7 +83,7 @@ void RenderInfo::update(int delta_ms) {
// visible, entrem (anim→1). Si no, primer eixim (anim→0) del lloc actual; // 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 // 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. // 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_) { if (DESIRED == visible_pos_) {
// Si el destí és OFF, no cal animar cap a un lloc — la barra ja ha // 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 // d'estar fora. Sense aquest guard, en arribar de BOTTOM→OFF la
@@ -111,38 +118,35 @@ void RenderInfo::update(int delta_ms) {
} }
void RenderInfo::refreshSegments() { void RenderInfo::refreshSegments() {
// [0] FPS (sempre visible mentre l'overlay apareix) // FPS (sempre visible mentre l'overlay apareix)
segments_[0].text = std::to_string(Screen::get().getLastFPS()) + " fps"; segments_[FPS_SEGMENT].text = std::to_string(Screen::get().getLastFPS()) + " fps";
segments_[0].visible = true; segments_[FPS_SEGMENT].visible = true;
segments_[0].mono_digits = false; segments_[FPS_SEGMENT].mono_digits = false;
// [1] Driver GPU (sempre visible) // Driver GPU (sempre visible)
const auto& driver = Screen::get().getGPUDriver(); const auto& driver = Screen::get().getGPUDriver();
segments_[1].text = std::string(" | ") + (driver.empty() ? std::string("sdl") : driver); segments_[DRIVER_SEGMENT].text = std::string(" | ") + (driver.empty() ? std::string("sdl") : driver);
segments_[1].visible = true; segments_[DRIVER_SEGMENT].visible = true;
segments_[1].mono_digits = false; segments_[DRIVER_SEGMENT].mono_digits = false;
// [2] Shader + preset (només amb shader activat) // Shader + preset (només amb shader activat). El caller ha triat el
if (Options::video.shader.enabled) { // preset actiu segons el shader; ací només el formatem.
const std::string& shader = Options::video.shader.current_shader; if (current_.shader_enabled) {
const std::string& preset = (shader == "crtpi") segments_[SHADER_SEGMENT].text = " | " + current_.shader_name + " " + prettify(current_.shader_preset);
? Options::video.shader.current_crtpi_preset
: Options::video.shader.current_postfx_preset;
segments_[2].text = " | " + shader + " " + prettify(preset);
} }
segments_[2].visible = Options::video.shader.enabled; segments_[SHADER_SEGMENT].visible = current_.shader_enabled;
segments_[2].mono_digits = false; segments_[SHADER_SEGMENT].mono_digits = false;
// [3] Temps de joc (togglejable i persistent) // Temps de joc (togglejable i persistent)
segments_[3].text = " | " + formatTime(elapsed_ms_); segments_[TIME_SEGMENT].text = " | " + formatTime(elapsed_ms_);
segments_[3].visible = Options::video.render_info_show_time; segments_[TIME_SEGMENT].visible = current_.show_time;
segments_[3].mono_digits = true; segments_[TIME_SEGMENT].mono_digits = true;
// Els tres primers segments baixem a minúscules per coherència estètica // 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ó. // amb la resta de la línia; el de temps manté dígits i puntuació.
segments_[0].text = prettify(segments_[0].text); segments_[FPS_SEGMENT].text = prettify(segments_[FPS_SEGMENT].text);
segments_[1].text = prettify(segments_[1].text); segments_[DRIVER_SEGMENT].text = prettify(segments_[DRIVER_SEGMENT].text);
segments_[2].text = prettify(segments_[2].text); segments_[SHADER_SEGMENT].text = prettify(segments_[SHADER_SEGMENT].text);
} }
auto RenderInfo::segmentWidth(const Segment& seg, int digit_cell_w) const -> int { auto RenderInfo::segmentWidth(const Segment& seg, int digit_cell_w) const -> int {
@@ -191,25 +195,13 @@ void RenderInfo::render() {
const int SEG_W = segmentWidth(seg, DIGIT_CELL); const int SEG_W = segmentWidth(seg, DIGIT_CELL);
if (seg.mono_digits) { if (seg.mono_digits) {
text_->writeMonoDigits(XI + 1, Y + 1, seg.text, UiColor::RENDER_INFO_SHADOW, 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, UiColor::RENDER_INFO_TEXT, DIGIT_CELL); text_->writeMonoDigits(XI, Y, seg.text, current_.text_color, DIGIT_CELL);
} else { } else {
text_->writeColored(XI + 1, Y + 1, seg.text, UiColor::RENDER_INFO_SHADOW); text_->writeColored(XI + SHADOW_OFFSET_PX, Y + SHADOW_OFFSET_PX, seg.text, current_.shadow_color);
text_->writeColored(XI, Y, seg.text, UiColor::RENDER_INFO_TEXT); text_->writeColored(XI, Y, seg.text, current_.text_color);
} }
cur_x += static_cast<float>(SEG_W) * Easing::outQuad(seg.anim); cur_x += static_cast<float>(SEG_W) * Easing::outQuad(seg.anim);
} }
} }
void RenderInfo::cyclePosition() {
const auto CURRENT = Options::video.render_info_position;
const int NEXT = (static_cast<int>(CURRENT) + 1) % 3;
Options::video.render_info_position = static_cast<Defaults::RenderInfoPosition>(NEXT);
Options::saveToFile();
}
void RenderInfo::toggleShowTime() {
Options::video.render_info_show_time = !Options::video.render_info_show_time;
Options::saveToFile();
}
+24 -13
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
@@ -11,9 +13,9 @@ class Text;
// Overlay de debug amb tres estats (OFF / TOP / BOTTOM). Mostra una barra // Overlay de debug amb tres estats (OFF / TOP / BOTTOM). Mostra una barra
// d'info dividida en segments independents (fps, driver, shader, temps) que // d'info dividida en segments independents (fps, driver, shader, temps) que
// apareixen i desapareixen suaument. La posició es commuta amb una tecla // apareixen i desapareixen suaument. No llig cap configuració; el caller
// (cicle OFF → TOP → BOTTOM) i el segment de temps es pot mostrar o amagar // (Director a init, GameScene a update) injecta cada frame l'estat desitjat
// per separat; les dos opcions persisteixen a config.yaml. // via Frame.
class RenderInfo { class RenderInfo {
public: public:
static auto get() -> RenderInfo&; static auto get() -> RenderInfo&;
@@ -23,18 +25,26 @@ class RenderInfo {
auto operator=(const RenderInfo&) -> RenderInfo& = delete; auto operator=(const RenderInfo&) -> RenderInfo& = delete;
auto operator=(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); // 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> text);
void update(int delta_ms, const Frame& frame);
void render(); 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 int HEIGHT = 10;
static constexpr float SLIDE_SPEED = 5.0F; // posició global (1/s) static constexpr float SLIDE_SPEED = 5.0F; // posició global (1/s)
static constexpr float SEG_SPEED = 6.0F; // segments individuals (1/s) static constexpr float SEG_SPEED = 6.0F; // segments individuals (1/s)
@@ -55,9 +65,10 @@ class RenderInfo {
void refreshSegments(); void refreshSegments();
[[nodiscard]] auto segmentWidth(const Segment& seg, int digit_cell_w) const -> int; [[nodiscard]] auto segmentWidth(const Segment& seg, int digit_cell_w) const -> int;
std::unique_ptr<Text> text_; std::shared_ptr<Text> text_;
Defaults::RenderInfoPosition visible_pos_{Defaults::RenderInfoPosition::OFF}; Defaults::RenderInfoPosition visible_pos_{Defaults::RenderInfoPosition::OFF};
float info_anim_{0.0F}; // 0..1, suavitzat amb outQuad al render float info_anim_{0.0F}; // 0..1, suavitzat amb outQuad al render
std::array<Segment, SEGMENT_COUNT> segments_{}; std::array<Segment, SEGMENT_COUNT> segments_{};
int elapsed_ms_{0}; int elapsed_ms_{0};
Frame current_{};
}; };
+5 -5
View File
@@ -10,7 +10,7 @@
#include <memory> #include <memory>
#include <ranges> #include <ranges>
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" #include "core/rendering/canvas.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
namespace { namespace {
@@ -53,7 +53,7 @@ void Screen::configure(const Config& config) {
subsystem_initialized_ = true; subsystem_initialized_ = true;
canvas_width_ = GameCanvas::WIDTH; canvas_width_ = GameCanvas::WIDTH;
canvas_height_ = GameCanvas::HEIGHT; canvas_height_ = GameCanvas::HEIGHT;
canvas_.assign(static_cast<std::size_t>(GameCanvas::WIDTH) * static_cast<std::size_t>(GameCanvas::HEIGHT), 0xFF000000U); canvas_.assign(static_cast<std::size_t>(GameCanvas::WIDTH) * static_cast<std::size_t>(GameCanvas::HEIGHT), Rendering::OPAQUE_ALPHA);
} }
destroySdl(); destroySdl();
config_ = config; config_ = config;
@@ -74,7 +74,7 @@ void Screen::destroySdl() {
// 2) Destruir el GPU device (vkDestroyInstance amb Wayland encara viu). // 2) Destruir el GPU device (vkDestroyInstance amb Wayland encara viu).
// 3) Destruir la finestra SDL (tanca la surface de Wayland al final). // 3) Destruir la finestra SDL (tanca la surface de Wayland al final).
if (shader_backend_ != nullptr) { if (shader_backend_ != nullptr) {
static_cast<Rendering::SDL3GPUShader*>(shader_backend_.get())->releaseWindow(); shader_backend_->releaseWindow();
} }
shader_backend_.reset(); shader_backend_.reset();
if (game_texture_ != nullptr) { if (game_texture_ != nullptr) {
@@ -106,7 +106,7 @@ void Screen::createSdl() {
// Primer intent: path SDL3 GPU. Si falla, fallback automàtic a SDL_Renderer. // Primer intent: path SDL3 GPU. Si falla, fallback automàtic a SDL_Renderer.
if (config_.gpu_acceleration) { if (config_.gpu_acceleration) {
auto backend = std::make_unique<Rendering::SDL3GPUShader>(); auto backend = Rendering::makeDefaultShaderBackend();
backend->setPreferredDriver(config_.preferred_driver); backend->setPreferredDriver(config_.preferred_driver);
backend->setVSync(config_.vsync); backend->setVSync(config_.vsync);
if (backend->init(window_, canvas_width_, canvas_height_)) { if (backend->init(window_, canvas_width_, canvas_height_)) {
@@ -215,7 +215,7 @@ void Screen::adjustWindowSize() {
} }
void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) { void Screen::clear(std::uint8_t r, std::uint8_t g, std::uint8_t b) {
const std::uint32_t COLOR = (0xFFU << 24) | (static_cast<std::uint32_t>(r) << 16) | (static_cast<std::uint32_t>(g) << 8) | static_cast<std::uint32_t>(b); const std::uint32_t COLOR = Rendering::OPAQUE_ALPHA | (static_cast<std::uint32_t>(r) << 16) | (static_cast<std::uint32_t>(g) << 8) | static_cast<std::uint32_t>(b);
std::ranges::fill(canvas_, COLOR); std::ranges::fill(canvas_, COLOR);
} }
@@ -0,0 +1,144 @@
#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 <metal_stdlib>
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<float> tex [[texture(0)]],
sampler samp [[sampler(0)]],
constant CrtPiUniforms& u [[buffer(0)]]) {
float2 tex_size = float2(u.texture_width, u.texture_height);
// 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) {
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__
@@ -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 <metal_stdlib>
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<float> 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__
@@ -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 <metal_stdlib>
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__
@@ -3,206 +3,17 @@
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
#include "core/rendering/sdl3gpu/shader_factory.hpp"
#include "core/rendering/sdl3gpu/viewport.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/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#endif #endif
namespace {
#ifdef __APPLE__
// NOLINTBEGIN(readability-identifier-naming)
const char* CRTPI_VERT_MSL_ = R"(
#include <metal_stdlib>
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 <metal_stdlib>
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<float> 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<const Uint8*>(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 { namespace Rendering {
CrtPiPipeline::~CrtPiPipeline() { cleanup(); } CrtPiPipeline::~CrtPiPipeline() { cleanup(); }
@@ -211,8 +22,8 @@ namespace Rendering {
device_ = device; device_ = device;
#ifdef __APPLE__ #ifdef __APPLE__
SDL_GPUShader* vert = createShaderMSL(device_, CRTPI_VERT_MSL_, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); SDL_GPUShader* vert = createShaderMSL(device_, Msl::kPostfxVert, "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* frag = createShaderMSL(device_, Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else #else
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); 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); SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
@@ -249,6 +60,10 @@ namespace Rendering {
std::cerr << "CrtPiPipeline: creació del pipeline ha fallat: " << SDL_GetError() << '\n'; std::cerr << "CrtPiPipeline: creació del pipeline ha fallat: " << SDL_GetError() << '\n';
return false; return false;
} }
// Fixem els defaults canònics abans del primer render; si Screen
// crida setParams més tard els sobreescriurà.
setParams(CrtPiParams{});
return true; return true;
} }
@@ -67,24 +67,9 @@ namespace Rendering {
SDL_GPUDevice* device_{nullptr}; SDL_GPUDevice* device_{nullptr};
SDL_GPUGraphicsPipeline* pipeline_{nullptr}; SDL_GPUGraphicsPipeline* pipeline_{nullptr};
CrtPiUniforms uniforms_{ // Inicialitzat amb value-init; init() el sobreescriu aplicant el
.scanline_weight = 6.0F, // CrtPiParams{} (defaults canònics, una sola font de veritat).
.scanline_gap_brightness = 0.12F, CrtPiUniforms uniforms_{};
.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,
};
}; };
} // namespace Rendering } // namespace Rendering
+11 -193
View File
@@ -4,203 +4,17 @@
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
#include "core/rendering/sdl3gpu/shader_factory.hpp"
#include "core/rendering/sdl3gpu/viewport.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_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h" #include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#endif #endif
namespace {
#ifdef __APPLE__
// NOLINTBEGIN(readability-identifier-naming)
const char* POSTFX_VERT_MSL_ = R"(
#include <metal_stdlib>
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 <metal_stdlib>
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<float> 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<const Uint8*>(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 { namespace Rendering {
SDL3GPUShader::~SDL3GPUShader() { cleanup(); } SDL3GPUShader::~SDL3GPUShader() { cleanup(); }
@@ -261,8 +75,8 @@ namespace Rendering {
auto SDL3GPUShader::createShaders() -> bool { auto SDL3GPUShader::createShaders() -> bool {
#ifdef __APPLE__ #ifdef __APPLE__
vertex_shader_ = createShaderMSL(device_, POSTFX_VERT_MSL_, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); vertex_shader_ = createShaderMSL(device_, Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
fragment_shader_ = createShaderMSL(device_, POSTFX_FRAG_MSL_, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); fragment_shader_ = createShaderMSL(device_, Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else #else
vertex_shader_ = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); 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); fragment_shader_ = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
@@ -493,4 +307,8 @@ namespace Rendering {
window_ = nullptr; window_ = nullptr;
} }
auto makeDefaultShaderBackend() -> std::unique_ptr<ShaderBackend> {
return std::make_unique<SDL3GPUShader>();
}
} // namespace Rendering } // namespace Rendering
@@ -60,7 +60,7 @@ namespace Rendering {
// Allibera la finestra del device sense destruir el device. Cal cridar-ho // Allibera la finestra del device sense destruir el device. Cal cridar-ho
// abans de SDL_DestroyWindow perquè el VkSurface tancat no sobrevisca la // abans de SDL_DestroyWindow perquè el VkSurface tancat no sobrevisca la
// finestra de Wayland (vkDestroyInstance cridaria Wayland amb surface invàlida). // 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 isReady() const -> bool override { return is_ready_; }
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; } [[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
@@ -0,0 +1,50 @@
#pragma once
#include <SDL3/SDL_gpu.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
// 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<const Uint8*>(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
+10
View File
@@ -3,6 +3,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint> #include <cstdint>
#include <memory>
#include <string> #include <string>
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
@@ -90,8 +91,17 @@ namespace Rendering {
// Destrueix tots els recursos GPU (pipeline, textura, buffer, sampler, device). // Destrueix tots els recursos GPU (pipeline, textura, buffer, sampler, device).
virtual void cleanup() = 0; 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 isReady() const -> bool = 0;
[[nodiscard]] virtual auto getDriverName() const -> std::string = 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<ShaderBackend>;
} // namespace Rendering } // namespace Rendering
@@ -27,7 +27,6 @@ class AnimatedSprite : public MovingSprite {
void update(float delta_time) override; void update(float delta_time) override;
auto animationIsCompleted() -> bool; auto animationIsCompleted() -> bool;
auto getCurrentAnimationSize() -> int { return static_cast<int>(animations_[current_animation_].frames.size()); }
void setCurrentAnimation(const std::string& name = "default"); void setCurrentAnimation(const std::string& name = "default");
void setCurrentAnimation(int index = 0); void setCurrentAnimation(int index = 0);
@@ -9,16 +9,18 @@ MovingSprite::MovingSprite() { Sprite::clear(); }
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface) MovingSprite::MovingSprite(std::shared_ptr<Surface> surface)
: Sprite(std::move(surface)) { Sprite::clear(); } : 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> surface, SDL_FRect pos) MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
: Sprite(std::move(surface), pos), : Sprite(std::move(surface), pos),
x_(pos.x), x_(pos.x),
y_(pos.y) { Sprite::pos_ = pos; } y_(pos.y) {}
MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip) MovingSprite::MovingSprite(std::shared_ptr<Surface> surface, SDL_FRect pos, SDL_FlipMode flip)
: Sprite(std::move(surface), pos), : Sprite(std::move(surface), pos),
x_(pos.x), x_(pos.x),
y_(pos.y), y_(pos.y),
flip_(flip) { Sprite::pos_ = pos; } flip_(flip) {}
void MovingSprite::clear() { void MovingSprite::clear() {
x_ = 0.0F; x_ = 0.0F;
+1 -5
View File
@@ -18,7 +18,7 @@ Sprite::Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect)
Sprite::Sprite(std::shared_ptr<Surface> surface) Sprite::Sprite(std::shared_ptr<Surface> surface)
: surface_(std::move(surface)), : surface_(std::move(surface)),
pos_{0.0F, 0.0F, static_cast<float>(surface_->getWidth()), static_cast<float>(surface_->getHeight())}, pos_{.x = 0.0F, .y = 0.0F, .w = static_cast<float>(surface_->getWidth()), .h = static_cast<float>(surface_->getHeight())},
clip_(pos_) {} clip_(pos_) {}
void Sprite::render() { void Sprite::render() {
@@ -43,7 +43,3 @@ void Sprite::clear() {
pos_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; 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}; clip_ = {.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
} }
void Sprite::update(float delta_time) {
(void)delta_time;
}
+3 -4
View File
@@ -16,7 +16,9 @@ class Sprite {
virtual ~Sprite() = default; 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(); virtual void render();
// Renderitza substituint l'índex source_color per target_color (ús típic: // Renderitza substituint l'índex source_color per target_color (ús típic:
// la classe Text, que recolora glifs monocroms per paràmetre). // 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 getPosition() const -> SDL_FRect { return pos_; }
[[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; } [[nodiscard]] auto getClip() const -> SDL_FRect { return clip_; }
[[nodiscard]] auto getSurface() const -> std::shared_ptr<Surface> { return surface_; } [[nodiscard]] auto getSurface() const -> std::shared_ptr<Surface> { return surface_; }
auto getRect() -> SDL_FRect& { return pos_; }
void setX(float x) { pos_.x = x; } void setX(float x) { pos_.x = x; }
void setY(float y) { pos_.y = y; } void setY(float y) { pos_.y = y; }
@@ -39,8 +40,6 @@ class Sprite {
void setPosition(float x, float y); void setPosition(float x, float y);
void setPosition(SDL_FPoint p); void setPosition(SDL_FPoint p);
void setPosition(SDL_FRect r) { pos_ = r; } 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(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}; } void setClip(float x, float y, float w, float h) { clip_ = SDL_FRect{.x = x, .y = y, .w = w, .h = h}; }
+23 -40
View File
@@ -6,8 +6,8 @@
#include <stdexcept> #include <stdexcept>
#include <vector> #include <vector>
#include "core/rendering/canvas.hpp"
#include "core/rendering/gif.hpp" #include "core/rendering/gif.hpp"
#include "core/rendering/screen.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
@@ -23,10 +23,10 @@ auto loadPaletteFromGif(const std::string& file_path) -> Palette {
} }
Palette 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()); const std::size_t COUNT = std::min(rgb.size(), palette.size());
for (std::size_t i = 0; i < COUNT; ++i) { for (std::size_t i = 0; i < COUNT; ++i) {
palette[i] = 0xFF000000U | rgb[i]; palette[i] = Rendering::OPAQUE_ALPHA | rgb[i];
} }
return palette; return palette;
} }
@@ -37,13 +37,9 @@ Surface::Surface(int w, int h)
} }
Surface::Surface(const std::string& file_path) { Surface::Surface(const std::string& file_path) {
SurfaceData loaded = loadSurface(file_path); // Carreguem el fitxer una única vegada i n'extraiem pixels i paleta de
surface_data_ = std::make_shared<SurfaceData>(std::move(loaded)); // passades independents sobre el mateix buffer en memòria — abans es
initializeSubPalette(sub_palette_); // parsejava dues voltes (loadSurface + loadPaletteFromGif).
palette_ = loadPaletteFromGif(file_path);
}
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData {
auto buffer = Resource::Helper::loadFile(file_path); auto buffer = Resource::Helper::loadFile(file_path);
if (buffer.empty()) { if (buffer.empty()) {
throw std::runtime_error("Surface: cannot open " + file_path); 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<Uint8[]>(new Uint8[raw_pixels.size()], std::default_delete<Uint8[]>()); auto pixels = std::shared_ptr<Uint8[]>(new Uint8[raw_pixels.size()], std::default_delete<Uint8[]>());
std::memcpy(pixels.get(), raw_pixels.data(), raw_pixels.size()); std::memcpy(pixels.get(), raw_pixels.data(), raw_pixels.size());
surface_data_ = std::make_shared<SurfaceData>(static_cast<int>(w), static_cast<int>(h), pixels);
initializeSubPalette(sub_palette_);
std::vector<std::uint32_t> 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] = Rendering::OPAQUE_ALPHA | rgb[i];
}
printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]"); printWithDots("Surface : ", getFileName(file_path), "[ LOADED ]");
return {static_cast<int>(w), static_cast<int>(h), pixels};
} }
void Surface::clear(Uint8 color) { void Surface::clear(Uint8 color) {
@@ -78,38 +81,18 @@ 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 { void Surface::render(int x, int y, const SDL_FRect* src_rect, SDL_FlipMode flip) const {
Uint32* dst = Screen::get().getCanvasArgb(); // Delegem a la versió amb remap: fent source==target==transparent_color_,
const int DST_W = Screen::get().getCanvasWidth(); // qualsevol píxel opaC tindrà COLOR != source_color (els transparents ja
const int DST_H = Screen::get().getCanvasHeight(); // s'han filtrat), així que REMAPPED = sub_palette_[COLOR] — idèntic al
// camí sense remap. Això evita duplicar el bucle de blit.
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0; renderWithColorReplace(x, y, transparent_color_, transparent_color_, src_rect, flip);
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
int sw = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : surface_data_->width;
int sh = (src_rect != nullptr) ? static_cast<int>(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]];
}
}
} }
void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 target_color, const SDL_FRect* src_rect, SDL_FlipMode flip) const { 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 auto CANVAS = Rendering::activeCanvas();
const int DST_W = Screen::get().getCanvasWidth(); Uint32* dst = CANVAS.pixels;
const int DST_H = Screen::get().getCanvasHeight(); const int DST_W = CANVAS.width;
const int DST_H = CANVAS.height;
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0; const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0; const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
-3
View File
@@ -45,8 +45,6 @@ class Surface {
explicit Surface(const std::string& file_path); explicit Surface(const std::string& file_path);
~Surface() = default; ~Surface() = default;
static auto loadSurface(const std::string& file_path) -> SurfaceData;
// Vobla la regió indicada al framebuffer ARGB del joc usant la paleta propia. // 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; 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 // Igual que render() però substituint els píxels amb valor source_color per
@@ -58,7 +56,6 @@ class Surface {
void putPixel(int x, int y, Uint8 color); void putPixel(int x, int y, Uint8 color);
[[nodiscard]] auto getPixel(int x, int y) const -> Uint8; [[nodiscard]] auto getPixel(int x, int y) const -> Uint8;
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
[[nodiscard]] auto getWidth() const -> int { return surface_data_->width; } [[nodiscard]] auto getWidth() const -> int { return surface_data_->width; }
[[nodiscard]] auto getHeight() const -> int { return surface_data_->height; } [[nodiscard]] auto getHeight() const -> int { return surface_data_->height; }
+21 -85
View File
@@ -47,26 +47,6 @@ auto Text::nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_
return cp; return cp;
} }
auto Text::codepointToUtf8(std::uint32_t cp) -> std::string {
std::string result;
if (cp < 0x80) {
result += static_cast<char>(cp);
} else if (cp < 0x800) {
result += static_cast<char>(0xC0 | (cp >> 6));
result += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
result += static_cast<char>(0xE0 | (cp >> 12));
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
result += static_cast<char>(0x80 | (cp & 0x3F));
} else {
result += static_cast<char>(0xF0 | (cp >> 18));
result += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
result += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
result += static_cast<char>(0x80 | (cp & 0x3F));
}
return result;
}
// Format .fnt: claus "box_width/box_height/columns/cell_spacing/row_spacing" // 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 // 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). // s'ordenen al png per ordre d'aparició al .fnt (glyph_index).
@@ -140,27 +120,10 @@ Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>&
offset_(text_file->offset) { offset_(text_file->offset) {
} }
// Dibuixa la cadena amb la tinta per defecte (codi 15 a la paleta del png). // Dibuixa la cadena amb la tinta per defecte. Els glifs amb codepoint no
// Els glifs amb codepoint no registrat es substitueixen pel glif '?'. // registrat es substitueixen pel glif '?'.
void Text::write(int x, int y, const std::string& text, int kerning, int length) { void Text::write(int x, int y, const std::string& text, int kerning, int length) {
int shift = 0; writeColored(x, y, text, DEFAULT_TEXT_COLOR, kerning, length);
int glyphs_done = 0;
std::size_t pos = 0;
sprite_->setY(static_cast<float>(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<float>(it->second.x), static_cast<float>(it->second.y), static_cast<float>(box_width_), static_cast<float>(box_height_));
sprite_->setX(static_cast<float>(x + shift));
sprite_->render(1, 15);
shift += it->second.w + kerning;
}
++glyphs_done;
}
} }
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int length) { void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int length) {
@@ -172,28 +135,16 @@ void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int
while (pos < text.size()) { while (pos < text.size()) {
if (length != -1 && glyphs_done >= length) { break; } if (length != -1 && glyphs_done >= length) { break; }
std::uint32_t cp = nextCodepoint(text, pos); std::uint32_t cp = nextCodepoint(text, pos);
auto it = offset_.find(cp); if (const Offset* off = resolveGlyph(cp); off != nullptr) {
if (it == offset_.end()) { it = offset_.find('?'); } sprite_->setClip(static_cast<float>(off->x), static_cast<float>(off->y), static_cast<float>(box_width_), static_cast<float>(box_height_));
if (it != offset_.end()) {
sprite_->setClip(static_cast<float>(it->second.x), static_cast<float>(it->second.y), static_cast<float>(box_width_), static_cast<float>(box_height_));
sprite_->setX(static_cast<float>(x + shift)); sprite_->setX(static_cast<float>(x + shift));
sprite_->render(1, color); sprite_->render(INK_COLOR_INDEX, color);
shift += it->second.w + kerning; shift += off->w + kerning;
} }
++glyphs_done; ++glyphs_done;
} }
} }
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) { 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 CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG); const bool SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
@@ -235,21 +186,20 @@ void Text::writeMonoDigits(int x, int y, const std::string& text, Uint8 color, i
sprite_->setY(static_cast<float>(y)); sprite_->setY(static_cast<float>(y));
while (pos < text.size()) { while (pos < text.size()) {
std::uint32_t cp = nextCodepoint(text, pos); std::uint32_t cp = nextCodepoint(text, pos);
auto it = offset_.find(cp); const Offset* off = resolveGlyph(cp);
if (it == offset_.end()) { it = offset_.find('?'); } if (off == nullptr) { continue; }
if (it == offset_.end()) { continue; }
const bool IS_DIGIT = (cp >= '0' && cp <= '9'); 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 CELL_W = IS_DIGIT ? digit_cell_w : GLYPH_W;
const int GLYPH_X = IS_DIGIT ? x + shift + ((CELL_W - GLYPH_W) / 2) : x + shift; const int GLYPH_X = IS_DIGIT ? x + shift + ((CELL_W - GLYPH_W) / 2) : x + shift;
sprite_->setClip(static_cast<float>(it->second.x), sprite_->setClip(static_cast<float>(off->x),
static_cast<float>(it->second.y), static_cast<float>(off->y),
static_cast<float>(box_width_), static_cast<float>(box_width_),
static_cast<float>(box_height_)); static_cast<float>(box_height_));
sprite_->setX(static_cast<float>(GLYPH_X)); sprite_->setX(static_cast<float>(GLYPH_X));
sprite_->render(1, color); sprite_->render(INK_COLOR_INDEX, color);
shift += CELL_W + kerning; shift += CELL_W + kerning;
} }
@@ -261,12 +211,11 @@ auto Text::lengthMonoDigits(const std::string& text, int digit_cell_w, int kerni
while (pos < text.size()) { while (pos < text.size()) {
std::uint32_t cp = nextCodepoint(text, pos); std::uint32_t cp = nextCodepoint(text, pos);
auto it = offset_.find(cp); const Offset* off = resolveGlyph(cp);
if (it == offset_.end()) { it = offset_.find('?'); } if (off == nullptr) { continue; }
if (it == offset_.end()) { continue; }
const bool IS_DIGIT = (cp >= '0' && cp <= '9'); 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; return shift > 0 ? shift - kerning : 0;
@@ -278,29 +227,16 @@ auto Text::length(const std::string& text, int kerning) const -> int {
while (pos < text.size()) { while (pos < text.size()) {
std::uint32_t cp = nextCodepoint(text, pos); std::uint32_t cp = nextCodepoint(text, pos);
auto it = offset_.find(cp); if (const Offset* off = resolveGlyph(cp); off != nullptr) {
if (it == offset_.end()) { it = offset_.find('?'); } shift += off->w + kerning;
if (it != offset_.end()) {
shift += it->second.w + kerning;
} }
} }
return shift > 0 ? shift - kerning : 0; return shift > 0 ? shift - kerning : 0;
} }
auto Text::glyphWidth(std::uint32_t codepoint, int kerning) const -> int { auto Text::resolveGlyph(std::uint32_t cp) const -> const Offset* {
auto it = offset_.find(codepoint); auto it = offset_.find(cp);
if (it == offset_.end()) { it = offset_.find('?'); } if (it == offset_.end()) { it = offset_.find('?'); }
if (it != offset_.end()) { return it->second.w + kerning; } return (it != offset_.end()) ? &it->second : nullptr;
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<float>(it->second.x),
.y = static_cast<float>(it->second.y),
.w = static_cast<float>(box_width_),
.h = static_cast<float>(box_height_)};
} }
+11 -6
View File
@@ -36,6 +36,13 @@ class Text {
static constexpr Uint8 CENTER_FLAG = 4; static constexpr Uint8 CENTER_FLAG = 4;
static constexpr Uint8 STROKE_FLAG = 8; 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>& surface, const std::string& text_file); Text(const std::shared_ptr<Surface>& surface, const std::string& text_file);
Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file); Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>& text_file);
~Text() = default; ~Text() = default;
@@ -47,8 +54,6 @@ class Text {
void write(int x, int y, const std::string& text, int kerning = 1, int length = -1); 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 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); 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 // Dibuixa amb amplada fixa només per als dígits '0'-'9' (cadascun centrat
@@ -59,15 +64,15 @@ class Text {
[[nodiscard]] auto length(const std::string& text, int kerning = 1) const -> int; [[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 lengthMonoDigits(const std::string& text, int digit_cell_w, int kerning = 1) const -> int;
[[nodiscard]] auto getCharacterSize() const -> int { return box_width_; } [[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<File>; static auto loadTextFile(const std::string& file_path) -> std::shared_ptr<File>;
static auto codepointToUtf8(std::uint32_t cp) -> std::string;
static auto nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t; static auto nextCodepoint(const std::string& s, std::size_t& pos) -> std::uint32_t;
private: 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> sprite_; std::unique_ptr<Sprite> sprite_;
int box_width_{0}; int box_width_{0};
int box_height_{0}; int box_height_{0};
+11 -2
View File
@@ -17,11 +17,13 @@
#include "core/rendering/render_info.hpp" #include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/surface.hpp" #include "core/rendering/surface.hpp"
#include "core/rendering/text.hpp"
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
#include "core/system/event_buffer.hpp" #include "core/system/event_buffer.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "game/scenes/game_scene.hpp" #include "game/scenes/game_scene.hpp"
#include "game/ui/ui_palette.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
Director::Director() { Director::Director() {
@@ -79,8 +81,15 @@ Director::Director() {
// JA_Music_t que continga, així que no queden streams orfes. // JA_Music_t que continga, així que no queden streams orfes.
Audio::init(Options::buildAudioConfig()); Audio::init(Options::buildAudioConfig());
RenderInfo::get().init(); // Font compartida pels overlays: construïm una sola instància de Text
Notifier::get().init(); // 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<Text>(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<GameScene>(); current_scene_ = std::make_unique<GameScene>();
last_ticks_ms_ = SDL_GetTicks(); last_ticks_ms_ = SDL_GetTicks();
+15
View File
@@ -553,6 +553,21 @@ namespace Options {
return Rendering::ShaderType::POSTFX; 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 { auto buildCrtPiParams() -> Rendering::CrtPiParams {
// Si el shader està deshabilitat, retornem defaults (el dispatch anirà per // Si el shader està deshabilitat, retornem defaults (el dispatch anirà per
// PostFX passthrough, però sent nets per si algú consulta aquests params). // PostFX passthrough, però sent nets per si algú consulta aquests params).
+4
View File
@@ -4,6 +4,7 @@
#include <vector> #include <vector>
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/rendering/render_info.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/shader_backend.hpp" #include "core/rendering/shader_backend.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
@@ -121,6 +122,9 @@ namespace Options {
[[nodiscard]] auto buildPostFXParams() -> Rendering::PostFXParams; [[nodiscard]] auto buildPostFXParams() -> Rendering::PostFXParams;
[[nodiscard]] auto buildCrtPiParams() -> Rendering::CrtPiParams; [[nodiscard]] auto buildCrtPiParams() -> Rendering::CrtPiParams;
[[nodiscard]] auto buildActiveShader() -> Rendering::ShaderType; [[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 // `::Audio` és la classe global d'àudio (no `Options::Audio`, que és la
// struct serialitzable). Retornem la seua Config per passar-la a init(). // struct serialitzable). Retornem la seua Config per passar-la a init().
[[nodiscard]] auto buildAudioConfig() -> ::Audio::Config; [[nodiscard]] auto buildAudioConfig() -> ::Audio::Config;
+2 -1
View File
@@ -15,6 +15,7 @@
#include "core/resources/resource_cache.hpp" #include "core/resources/resource_cache.hpp"
#include "game/entities/player.hpp" #include "game/entities/player.hpp"
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include "game/options.hpp"
#include "game/ui/ui_palette.hpp" #include "game/ui/ui_palette.hpp"
#include "utils/defaults.hpp" #include "utils/defaults.hpp"
#include "utils/defines.hpp" #include "utils/defines.hpp"
@@ -60,7 +61,7 @@ void GameScene::update(int delta_ms) {
const float DT = static_cast<float>(delta_ms) / 1000.0F; const float DT = static_cast<float>(delta_ms) / 1000.0F;
for (auto& player : players_) { player->update(DT); } 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); Notifier::get().update(delta_ms);
} }
+12 -3
View File
@@ -2,11 +2,14 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <string>
#include "core/rendering/notifier.hpp"
// Temes per a les notificacions de pantalla. Un "preset" captura els dos // 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 // colors (text i fons, índexs sobre la paleta "ui") i el temps de visibilitat
// en mil·lisegons. Qualsevol component que cride Notifier::notify(text) // en mil·lisegons. Per a llançar una notificació amb un preset concret,
// rebrà l'estil DEFAULT; si necessita un altre look, pot passar el preset per // cridar Notification::notify(text, preset); si s'omet, s'usa DEFAULT.
// nom o especificar els camps un a un.
namespace Notification { namespace Notification {
struct Preset { struct Preset {
@@ -34,4 +37,10 @@ namespace Notification {
.time_ms = 4000, .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 } // namespace Notification
+1
View File
@@ -30,6 +30,7 @@ namespace Defaults {
TOP = 1, TOP = 1,
BOTTOM = 2, BOTTOM = 2,
}; };
constexpr int RENDER_INFO_POSITION_COUNT = 3;
namespace Window { namespace Window {
constexpr int ZOOM = 3; constexpr int ZOOM = 3;