merge: migració PostFX a versió analítica sense supersampling
This commit is contained in:
@@ -153,14 +153,12 @@ if(NOT APPLE AND NOT EMSCRIPTEN)
|
||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||
"${HEADERS_DIR}/upscale_frag_spv.h"
|
||||
"${HEADERS_DIR}/downscale_frag_spv.h"
|
||||
"${HEADERS_DIR}/crtpi_frag_spv.h"
|
||||
)
|
||||
set(ALL_SHADER_SOURCES
|
||||
"${SHADERS_DIR}/postfx.vert"
|
||||
"${SHADERS_DIR}/postfx.frag"
|
||||
"${SHADERS_DIR}/upscale.frag"
|
||||
"${SHADERS_DIR}/downscale.frag"
|
||||
"${SHADERS_DIR}/crtpi_frag.glsl"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ BUILDDIR := build
|
||||
# ==============================================================================
|
||||
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
|
||||
SHADERS_DIR := $(DIR_ROOT)data/shaders
|
||||
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
|
||||
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu/spv
|
||||
ifeq ($(OS),Windows_NT)
|
||||
GLSLC := $(shell where glslc 2>NUL)
|
||||
else
|
||||
|
||||
@@ -27,9 +27,6 @@ keys:
|
||||
- id: toggle_aspect_ratio
|
||||
code: "F5"
|
||||
desc: "Aspecte 4:3 / pixels quadrats"
|
||||
- id: toggle_supersampling
|
||||
code: "F6"
|
||||
desc: "Activa/desactiva supersampling"
|
||||
- id: next_shader
|
||||
code: "F7"
|
||||
desc: "Tipus de shader (PostFX / CRT-Pi)"
|
||||
|
||||
@@ -26,7 +26,6 @@ menu:
|
||||
screen: "Pantalla"
|
||||
shader: "Shader"
|
||||
aspect_4_3: "Aspecte 4:3"
|
||||
supersampling: "Supersampling"
|
||||
vsync: "Vsync"
|
||||
scaling_mode: "Escala"
|
||||
shader_type: "Tipus shader"
|
||||
@@ -79,8 +78,6 @@ notifications:
|
||||
shader_off: "Shader off"
|
||||
aspect_43: "4:3 CRT"
|
||||
aspect_square: "Píxels quadrats"
|
||||
ss_on: "Supersampling on"
|
||||
ss_off: "Supersampling off"
|
||||
preset_fmt: "Preset: %s"
|
||||
filter_linear: "Filtre: linear"
|
||||
filter_nearest: "Filtre: nearest"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#version 450
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D source;
|
||||
|
||||
layout(set = 3, binding = 0) uniform DownscaleUniforms {
|
||||
int algorithm; // 0 = Lanczos2 (ventana 2, ±2 taps), 1 = Lanczos3 (ventana 3, ±3 taps)
|
||||
float pad0;
|
||||
float pad1;
|
||||
float pad2;
|
||||
} u;
|
||||
|
||||
// Kernel Lanczos normalizado: sinc(t) * sinc(t/a) para |t| < a, 0 fuera.
|
||||
float lanczos(float t, float a) {
|
||||
t = abs(t);
|
||||
if (t < 0.0001) { return 1.0; }
|
||||
if (t >= a) { return 0.0; }
|
||||
const float PI = 3.14159265358979;
|
||||
float pt = PI * t;
|
||||
return (a * sin(pt) * sin(pt / a)) / (pt * pt);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 src_size = vec2(textureSize(source, 0));
|
||||
// Posición en coordenadas de texel (centros de texel en N+0.5)
|
||||
vec2 p = v_uv * src_size;
|
||||
vec2 p_floor = floor(p);
|
||||
|
||||
float a = (u.algorithm == 0) ? 2.0 : 3.0;
|
||||
int win = int(a);
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
float weight_sum = 0.0;
|
||||
|
||||
for (int j = -win; j <= win; j++) {
|
||||
for (int i = -win; i <= win; i++) {
|
||||
// Centro del texel (i,j) relativo a p_floor
|
||||
vec2 tap_center = p_floor + vec2(float(i), float(j)) + 0.5;
|
||||
vec2 offset = tap_center - p;
|
||||
float w = lanczos(offset.x, a) * lanczos(offset.y, a);
|
||||
color += texture(source, tap_center / src_size) * w;
|
||||
weight_sum += w;
|
||||
}
|
||||
}
|
||||
|
||||
out_color = (weight_sum > 0.0) ? (color / weight_sum) : vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
+47
-25
@@ -6,7 +6,9 @@
|
||||
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
|
||||
//
|
||||
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
|
||||
// (8 floats, 32 bytes, std140/scalar layout).
|
||||
// (16 floats = 4 × vec4 = 64 bytes, std140/scalar layout).
|
||||
// IMPORTANT: Qualsevol canvi ací cal replicar-lo a mà a
|
||||
// source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h (no hi ha generador).
|
||||
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
@@ -15,7 +17,7 @@ layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||
|
||||
layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float chroma_min; // intensitat mínima de l'aberració cromàtica
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
@@ -24,10 +26,28 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||
float bleeding;
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — 48 bytes total (3 × 16)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||
float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
|
||||
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||||
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
|
||||
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
|
||||
float scan_edge_soft; // 0 = step dur; 1 = suavitzat d'1 píxel físic (estil crtpi)
|
||||
float pad3; // padding per tancar a 64 bytes (4 × vec4)
|
||||
} u;
|
||||
|
||||
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||||
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
|
||||
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
|
||||
float sampleBilinearX(vec2 uv_target, int channel) {
|
||||
vec2 tex_size = vec2(textureSize(scene, 0));
|
||||
float px = uv_target.x * tex_size.x - 0.5;
|
||||
float p_floor = floor(px);
|
||||
float f = px - p_floor;
|
||||
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
|
||||
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
|
||||
return mix(c0[channel], c1[channel], f);
|
||||
}
|
||||
|
||||
// YCbCr helpers for NTSC bleeding
|
||||
vec3 rgb_to_ycc(vec3 rgb) {
|
||||
return vec3(
|
||||
@@ -69,11 +89,11 @@ void main() {
|
||||
vec3 base = texture(scene, uv).rgb;
|
||||
|
||||
// Sangrado NTSC — difuminado horizontal de crominancia.
|
||||
// step = 1 pixel lógico de juego en UV (corrige SS: textureSize.x = game_w * oversample).
|
||||
// step = 1 pixel lógico de juego en UV.
|
||||
vec3 colour;
|
||||
if (u.bleeding > 0.0) {
|
||||
float tw = float(textureSize(scene, 0).x);
|
||||
float step = u.oversample / tw; // 1 pixel lógico en UV
|
||||
float step = 1.0 / tw; // 1 pixel lógico en UV
|
||||
vec3 ycc = rgb_to_ycc(base);
|
||||
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
|
||||
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
|
||||
@@ -85,10 +105,14 @@ void main() {
|
||||
colour = base;
|
||||
}
|
||||
|
||||
// Aberración cromática (drift animado con time para efecto NTSC real)
|
||||
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3));
|
||||
colour.r = texture(scene, uv + vec2(ca, 0.0)).r;
|
||||
colour.b = texture(scene, uv - vec2(ca, 0.0)).b;
|
||||
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
|
||||
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
|
||||
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
|
||||
if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
|
||||
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
|
||||
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
|
||||
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
|
||||
}
|
||||
|
||||
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||||
if (u.gamma_strength > 0.0) {
|
||||
@@ -96,22 +120,20 @@ void main() {
|
||||
colour = mix(colour, lin, u.gamma_strength);
|
||||
}
|
||||
|
||||
// Scanlines — proporción 2/3 brillantes + 1/3 oscuras por fila lógica.
|
||||
// Casos especiales: 1 subfila → sin efecto; 2 subfilas → 1+1 (50/50).
|
||||
// Constantes ajustables:
|
||||
const float SCAN_DARK_RATIO = 0.333; // fracción de subfilas oscuras (ps >= 3)
|
||||
const float SCAN_DARK_FLOOR = 0.42; // multiplicador de brillo de subfilas oscuras
|
||||
// Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
|
||||
// franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
|
||||
// suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
|
||||
// continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
|
||||
if (u.scanline_strength > 0.0) {
|
||||
float ps = max(1.0, round(u.pixel_scale));
|
||||
float frac_in_row = fract(uv.y * u.screen_height);
|
||||
float row_pos = floor(frac_in_row * ps);
|
||||
// bright_rows: cuántas subfilas son brillantes
|
||||
// ps==1 → ps (todo brillante → is_dark nunca se activa)
|
||||
// ps==2 → 1 brillante + 1 oscura
|
||||
// ps>=3 → floor(ps * (1 - DARK_RATIO)) brillantes
|
||||
float bright_rows = (ps < 2.0) ? ps : ((ps < 3.0) ? 1.0 : floor(ps * (1.0 - SCAN_DARK_RATIO)));
|
||||
float is_dark = step(bright_rows, row_pos);
|
||||
float scan = mix(1.0, SCAN_DARK_FLOOR, is_dark);
|
||||
float ps = max(u.pixel_scale, 1.0);
|
||||
float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
|
||||
float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
|
||||
float d = abs(sub - dark_center);
|
||||
d = min(d, 1.0 - d); // wrap a la fila següent
|
||||
float half_width = u.scan_dark_ratio * 0.5;
|
||||
float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
|
||||
float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
|
||||
float scan = mix(1.0, u.scan_dark_floor, band);
|
||||
colour *= mix(1.0, scan, u.scanline_strength);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,17 +49,17 @@ class Audio {
|
||||
static void update(); // Actualización del sistema de audio
|
||||
|
||||
// --- Control de música ---
|
||||
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
|
||||
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
|
||||
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
|
||||
// --- Control de sonidos ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
||||
void playSound(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
||||
void playSound(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
|
||||
// --- Control de volumen (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace GlobalInputs {
|
||||
static bool fullscreen_prev = false;
|
||||
static bool shader_prev = false;
|
||||
static bool aspect_prev = false;
|
||||
static bool ss_prev = false;
|
||||
static bool next_shader_prev = false;
|
||||
static bool next_preset_prev = false;
|
||||
static bool texture_filter_prev = false;
|
||||
@@ -61,11 +60,6 @@ namespace GlobalInputs {
|
||||
Screen::get()->toggleAspectRatio();
|
||||
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
|
||||
});
|
||||
consumed |= edgeTrigger("toggle_supersampling", ss_prev, [] {
|
||||
if (Screen::get()->toggleSupersampling()) {
|
||||
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
|
||||
}
|
||||
});
|
||||
consumed |= edgeTrigger("next_shader", next_shader_prev, [] {
|
||||
if (Screen::get()->nextShaderType()) {
|
||||
char msg[64];
|
||||
|
||||
@@ -200,11 +200,6 @@ namespace Menu {
|
||||
} else { Screen::get()->nextPreset();
|
||||
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::TOGGLE, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] {
|
||||
if (!Options::video.shader_enabled) { return false;
|
||||
}
|
||||
const char* name = Screen::get()->getActiveShaderName();
|
||||
return name && std::string(name) == "POSTFX"; }});
|
||||
#endif
|
||||
|
||||
// Informació de render
|
||||
|
||||
@@ -179,12 +179,6 @@ void Screen::initShaders() {
|
||||
shader_backend_->setVSync(Options::video.vsync);
|
||||
shader_backend_->setTextureFilter(Options::video.texture_filter);
|
||||
shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
|
||||
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
|
||||
|
||||
if (Options::video.supersampling) {
|
||||
shader_backend_->setOversample(3);
|
||||
}
|
||||
|
||||
shader_backend_->setInternalResolution(Options::video.internal_resolution);
|
||||
|
||||
// Resol el shader actiu des del config
|
||||
@@ -325,24 +319,6 @@ void Screen::toggleShaders() {
|
||||
}
|
||||
}
|
||||
|
||||
auto Screen::toggleSupersampling() -> bool {
|
||||
// SS només té sentit amb shaders on i pipeline PostFX (el Lanczos downscale
|
||||
// i el camí SS s'apliquen al pas de PostFX; CRTPI fa el seu propi
|
||||
// submostreig intern i no usa aquesta via).
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return false;
|
||||
}
|
||||
if (!Options::video.shader_enabled) {
|
||||
return false;
|
||||
}
|
||||
if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) {
|
||||
return false;
|
||||
}
|
||||
Options::video.supersampling = !Options::video.supersampling;
|
||||
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Screen::toggleAspectRatio() {
|
||||
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
|
||||
if (shader_backend_) {
|
||||
@@ -516,12 +492,16 @@ void Screen::applyCurrentPostFXPreset() {
|
||||
Rendering::PostFXParams p;
|
||||
p.vignette = preset.vignette;
|
||||
p.scanlines = preset.scanlines;
|
||||
p.chroma = preset.chroma;
|
||||
p.chroma_min = preset.chroma_min;
|
||||
p.chroma_max = preset.chroma_max;
|
||||
p.mask = preset.mask;
|
||||
p.gamma = preset.gamma;
|
||||
p.curvature = preset.curvature;
|
||||
p.bleeding = preset.bleeding;
|
||||
p.flicker = preset.flicker;
|
||||
p.scan_dark_ratio = preset.scan_dark_ratio;
|
||||
p.scan_dark_floor = preset.scan_dark_floor;
|
||||
p.scan_edge_soft = preset.scan_edge_soft;
|
||||
shader_backend_->setPostFXParams(p);
|
||||
}
|
||||
|
||||
@@ -572,10 +552,7 @@ void Screen::updateRenderInfo() {
|
||||
shader_seg = " - " + toLower(getActiveShaderName()) + " " + toLower(getCurrentPresetName());
|
||||
}
|
||||
|
||||
// Segment 2: supersampling indicator
|
||||
const char* ss_seg = (Options::video.shader_enabled && Options::video.supersampling) ? " (ss)" : nullptr;
|
||||
|
||||
// Segment 3: hora (només si show_time)
|
||||
// Segment 2: hora (només si show_time)
|
||||
char time_buf[32] = {0};
|
||||
if (Options::render_info.show_time) {
|
||||
Uint32 elapsed = SDL_GetTicks() - START_TICKS;
|
||||
@@ -585,14 +562,14 @@ void Screen::updateRenderInfo() {
|
||||
snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis);
|
||||
}
|
||||
|
||||
// Dígits en mono a FPS (segment 0) i TEMPS (segment 3): els dígits canvien
|
||||
// Dígits en mono a FPS (segment 0) i TEMPS (segment 2): els dígits canvien
|
||||
// contínuament mentre els símbols del voltant ("fps", ":", ".", " - ") no
|
||||
Overlay::setRenderInfoSegments(
|
||||
fps_driver.c_str(),
|
||||
shader_seg.empty() ? nullptr : shader_seg.c_str(),
|
||||
ss_seg,
|
||||
(time_buf[0] != 0) ? time_buf : nullptr,
|
||||
0b1001);
|
||||
nullptr,
|
||||
0b0101);
|
||||
}
|
||||
|
||||
void Screen::applyFallbackPresentation() {
|
||||
|
||||
@@ -30,7 +30,6 @@ class Screen {
|
||||
// no es complia. Els callers (F-keys, menú) poden suprimir notificacions
|
||||
// o feedback quan la crida no ha tingut efecte.
|
||||
void toggleShaders();
|
||||
auto toggleSupersampling() -> bool; // false si GPU off / shaders off / actiu != POSTFX
|
||||
void toggleAspectRatio();
|
||||
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
|
||||
void toggleVSync();
|
||||
|
||||
@@ -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,168 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask,
|
||||
// gamma, curvature, bleeding i flicker. Els paràmetres venen via uniforms.
|
||||
//
|
||||
// IMPORTANT: mantenir sincronitzat a mà amb data/shaders/postfx.frag. SDL3 GPU
|
||||
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
|
||||
// canvi a la struct d'uniforms o a la lògica del GLSL cal replicar-lo ací al
|
||||
// mateix commit. Mida total = 64 bytes (4 × vec4).
|
||||
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_min;
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
float gamma_strength;
|
||||
float curvature;
|
||||
float bleeding;
|
||||
float pixel_scale;
|
||||
float time;
|
||||
float flicker;
|
||||
float chroma_max;
|
||||
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||||
float scan_dark_ratio;
|
||||
float scan_dark_floor;
|
||||
float scan_edge_soft;
|
||||
float pad3;
|
||||
};
|
||||
|
||||
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||||
// NEAREST quan l'offset de chroma és subpíxel.
|
||||
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
|
||||
float2 tex_size = float2(scene.get_width(), scene.get_height());
|
||||
float px = uv_target.x * tex_size.x - 0.5f;
|
||||
float p_floor = floor(px);
|
||||
float f = px - p_floor;
|
||||
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
|
||||
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
|
||||
return mix(c0[channel], c1[channel], f);
|
||||
}
|
||||
|
||||
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 = 1.0f / 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;
|
||||
}
|
||||
|
||||
// Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
|
||||
// queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
|
||||
// NEAREST sampler amb offsets subpíxel.
|
||||
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
|
||||
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
|
||||
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
|
||||
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
|
||||
}
|
||||
|
||||
if (u.gamma_strength > 0.0f) {
|
||||
float3 lin = pow(colour, float3(2.4f));
|
||||
colour = mix(colour, lin, u.gamma_strength);
|
||||
}
|
||||
|
||||
// Scanlines — 3 subpíxels per fila lògica (2 brillants + 1 fosca). Transició
|
||||
// suavitzada amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge
|
||||
// analític continu). scan_edge_soft = 0 recupera el step dur de l'original.
|
||||
if (u.scanline_strength > 0.0f) {
|
||||
float ps = max(u.pixel_scale, 1.0f);
|
||||
float sub = fract(uv.y * u.screen_height);
|
||||
float dark_center = 1.0f - u.scan_dark_ratio * 0.5f;
|
||||
float d = abs(sub - dark_center);
|
||||
d = min(d, 1.0f - d);
|
||||
float half_width = u.scan_dark_ratio * 0.5f;
|
||||
float softness = u.scan_edge_soft * 0.5f / ps;
|
||||
float band = 1.0f - smoothstep(half_width - softness, half_width + softness, d);
|
||||
float scan = mix(1.0f, u.scan_dark_floor, band);
|
||||
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__
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
// Fragment shader d'upscale (mostreig directe). S'usa per al pas de resolució
|
||||
// interna (scene_texture → internal_texture) quan internal_resolution > 1.
|
||||
namespace Rendering::Msl {
|
||||
|
||||
inline constexpr const char* kUpscaleFrag = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
struct VertOut { float4 pos [[position]]; float2 uv; };
|
||||
fragment float4 upscale_fs(VertOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler smp [[sampler(0)]])
|
||||
{
|
||||
return scene.sample(smp, in.uv);
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
@@ -9,360 +9,17 @@
|
||||
|
||||
#ifndef __APPLE__
|
||||
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
|
||||
#include "core/rendering/sdl3gpu/spv/downscale_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/upscale_frag_spv.h"
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
// ============================================================================
|
||||
// MSL shaders (Metal Shading Language) — macOS
|
||||
// ============================================================================
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
static 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;
|
||||
}
|
||||
)";
|
||||
|
||||
static 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; // 1.0 = sin SS, 3.0 = 3× supersampling
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||
};
|
||||
|
||||
// YCbCr helpers for NTSC bleeding
|
||||
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;
|
||||
|
||||
// Curvatura barrel CRT
|
||||
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;
|
||||
}
|
||||
|
||||
// Muestra base
|
||||
float3 base = scene.sample(samp, uv).rgb;
|
||||
|
||||
// Sangrado NTSC — difuminado horizontal de crominancia.
|
||||
// step = 1 pixel de juego en espacio UV (corrige SS: scene.get_width() = game_w * oversample).
|
||||
float3 colour;
|
||||
if (u.bleeding > 0.0f) {
|
||||
float tw = float(scene.get_width());
|
||||
float step = u.oversample / tw; // 1 pixel lógico en UV
|
||||
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;
|
||||
}
|
||||
|
||||
// Aberración cromática (drift animado con time para efecto NTSC real)
|
||||
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;
|
||||
|
||||
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||||
if (u.gamma_strength > 0.0f) {
|
||||
float3 lin = pow(colour, float3(2.4f));
|
||||
colour = mix(colour, lin, u.gamma_strength);
|
||||
}
|
||||
|
||||
// Scanlines — proporción 2/3 brillantes + 1/3 oscuras por fila lógica.
|
||||
// Casos especiales: 1 subfila → sin efecto; 2 subfilas → 1+1 (50/50).
|
||||
// Constantes ajustables:
|
||||
const float SCAN_DARK_RATIO = 0.333f; // fracción de subfilas oscuras (ps >= 3)
|
||||
const float SCAN_DARK_FLOOR = 0.42f; // multiplicador de brillo de subfilas oscuras
|
||||
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);
|
||||
}
|
||||
|
||||
// Viñeta
|
||||
float2 d = uv - 0.5f;
|
||||
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
|
||||
colour *= clamp(vignette, 0.0f, 1.0f);
|
||||
|
||||
// Máscara de fósforo RGB — después de scanlines (orden original):
|
||||
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
|
||||
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);
|
||||
}
|
||||
|
||||
// Parpadeo de fósforo CRT (~50 Hz)
|
||||
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);
|
||||
}
|
||||
)";
|
||||
static const char* UPSCALE_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
struct VertOut { float4 pos [[position]]; float2 uv; };
|
||||
fragment float4 upscale_fs(VertOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler smp [[sampler(0)]])
|
||||
{
|
||||
return scene.sample(smp, in.uv);
|
||||
}
|
||||
)";
|
||||
|
||||
static const char* DOWNSCALE_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
struct VertOut { float4 pos [[position]]; float2 uv; };
|
||||
struct DownscaleUniforms { int algorithm; float pad0; float pad1; float pad2; };
|
||||
|
||||
static float lanczos_w(float t, float a) {
|
||||
t = abs(t);
|
||||
if (t < 0.0001f) { return 1.0f; }
|
||||
if (t >= a) { return 0.0f; }
|
||||
const float PI = 3.14159265358979f;
|
||||
float pt = PI * t;
|
||||
return (a * sin(pt) * sin(pt / a)) / (pt * pt);
|
||||
}
|
||||
|
||||
fragment float4 downscale_fs(VertOut in [[stage_in]],
|
||||
texture2d<float> source [[texture(0)]],
|
||||
sampler smp [[sampler(0)]],
|
||||
constant DownscaleUniforms& u [[buffer(0)]])
|
||||
{
|
||||
float2 src_size = float2(source.get_width(), source.get_height());
|
||||
float2 p = in.uv * src_size;
|
||||
float2 p_floor = floor(p);
|
||||
float a = (u.algorithm == 0) ? 2.0f : 3.0f;
|
||||
int win = int(a);
|
||||
float4 color = float4(0.0f);
|
||||
float weight_sum = 0.0f;
|
||||
for (int j = -win; j <= win; j++) {
|
||||
for (int i = -win; i <= win; i++) {
|
||||
float2 tap_center = p_floor + float2(float(i), float(j)) + 0.5f;
|
||||
float2 offset = tap_center - p;
|
||||
float w = lanczos_w(offset.x, a) * lanczos_w(offset.y, a);
|
||||
color += source.sample(smp, tap_center / src_size) * w;
|
||||
weight_sum += w;
|
||||
}
|
||||
}
|
||||
return (weight_sum > 0.0f) ? (color / weight_sum) : float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
)";
|
||||
static const char* CRTPI_FRAG_MSL = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
struct PostVOut {
|
||||
float4 pos [[position]];
|
||||
float2 uv;
|
||||
};
|
||||
|
||||
struct CrtPiUniforms {
|
||||
// vec4 #0
|
||||
float scanline_weight;
|
||||
float scanline_gap_brightness;
|
||||
float bloom_factor;
|
||||
float input_gamma;
|
||||
// vec4 #1
|
||||
float output_gamma;
|
||||
float mask_brightness;
|
||||
float curvature_x;
|
||||
float curvature_y;
|
||||
// vec4 #2
|
||||
int mask_type;
|
||||
int enable_scanlines;
|
||||
int enable_multisample;
|
||||
int enable_gamma;
|
||||
// vec4 #3
|
||||
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 // __APPLE__
|
||||
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
|
||||
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
|
||||
#include "core/rendering/sdl3gpu/msl/postfx_vert.msl.h"
|
||||
#include "core/rendering/sdl3gpu/msl/upscale_frag.msl.h"
|
||||
#endif
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
@@ -396,7 +53,6 @@ namespace Rendering {
|
||||
game_width_ = static_cast<int>(fw);
|
||||
game_height_ = static_cast<int>(fh);
|
||||
uniforms_.screen_height = static_cast<float>(game_height_);
|
||||
uniforms_.oversample = static_cast<float>(oversample_);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 1. Create GPU device (solo si no existe ya)
|
||||
@@ -461,9 +117,6 @@ namespace Rendering {
|
||||
// textura a nullptr i el pipeline ometrà la còpia.
|
||||
recreateInternalTexture();
|
||||
|
||||
// scaled_texture_ se creará en el primer render() una vez conocido el zoom de ventana
|
||||
ss_factor_ = 0;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 4. Create upload transfer buffer (CPU → GPU, always game resolution)
|
||||
// ----------------------------------------------------------------
|
||||
@@ -525,7 +178,7 @@ namespace Rendering {
|
||||
}
|
||||
|
||||
is_initialized_ = true;
|
||||
std::cout << "GPU Shader : initialized OK — game " << game_width_ << 'x' << game_height_ << ", oversample " << oversample_ << '\n';
|
||||
std::cout << "GPU Shader : initialized OK — game " << game_width_ << 'x' << game_height_ << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -537,8 +190,8 @@ namespace Rendering {
|
||||
|
||||
// ---- PostFX pipeline (scene/scaled → swapchain) ----
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* vert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
@@ -581,8 +234,8 @@ namespace Rendering {
|
||||
|
||||
// ---- Upscale pipeline (scene → scaled_texture_, nearest) ----
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* uvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* ufrag = createShaderMSL(device_, UPSCALE_FRAG_MSL, "upscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||
SDL_GPUShader* uvert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* ufrag = createShaderMSL(device_, Rendering::Msl::kUpscaleFrag, "upscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||
#else
|
||||
SDL_GPUShader* uvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* ufrag = createShaderSPIRV(device_, kupscale_frag_spv, kupscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
|
||||
@@ -617,83 +270,6 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- PostFX offscreen pipeline (scaled_texture_ → postfx_texture_, B8G8R8A8) ----
|
||||
// Mismos shaders que pipeline_ pero con formato de salida B8G8R8A8_UNORM para textura intermedia.
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* ofvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* offrag = createShaderMSL(device_, POSTFX_FRAG_MSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
SDL_GPUShader* ofvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* offrag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
|
||||
if ((ofvert == nullptr) || (offrag == nullptr)) {
|
||||
SDL_Log("SDL3GPUShader: failed to compile PostFX offscreen shaders");
|
||||
if (ofvert != nullptr) { SDL_ReleaseGPUShader(device_, ofvert); }
|
||||
if (offrag != nullptr) { SDL_ReleaseGPUShader(device_, offrag); }
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetDescription offscreen_color_target = {};
|
||||
offscreen_color_target.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||
offscreen_color_target.blend_state = no_blend;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo offscreen_pipe_info = {};
|
||||
offscreen_pipe_info.vertex_shader = ofvert;
|
||||
offscreen_pipe_info.fragment_shader = offrag;
|
||||
offscreen_pipe_info.vertex_input_state = no_input;
|
||||
offscreen_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
offscreen_pipe_info.target_info.num_color_targets = 1;
|
||||
offscreen_pipe_info.target_info.color_target_descriptions = &offscreen_color_target;
|
||||
|
||||
postfx_offscreen_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &offscreen_pipe_info);
|
||||
|
||||
SDL_ReleaseGPUShader(device_, ofvert);
|
||||
SDL_ReleaseGPUShader(device_, offrag);
|
||||
|
||||
if (postfx_offscreen_pipeline_ == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: PostFX offscreen pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Downscale pipeline (postfx_texture_ → swapchain, Lanczos) ----
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* dvert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* dfrag = createShaderMSL(device_, DOWNSCALE_FRAG_MSL, "downscale_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
SDL_GPUShader* dvert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* dfrag = createShaderSPIRV(device_, kdownscale_frag_spv, kdownscale_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#endif
|
||||
|
||||
if ((dvert == nullptr) || (dfrag == nullptr)) {
|
||||
SDL_Log("SDL3GPUShader: failed to compile downscale shaders");
|
||||
if (dvert != nullptr) { SDL_ReleaseGPUShader(device_, dvert); }
|
||||
if (dfrag != nullptr) { SDL_ReleaseGPUShader(device_, dfrag); }
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_GPUColorTargetDescription downscale_color_target = {};
|
||||
downscale_color_target.format = SWAPCHAIN_FMT;
|
||||
downscale_color_target.blend_state = no_blend;
|
||||
|
||||
SDL_GPUGraphicsPipelineCreateInfo downscale_pipe_info = {};
|
||||
downscale_pipe_info.vertex_shader = dvert;
|
||||
downscale_pipe_info.fragment_shader = dfrag;
|
||||
downscale_pipe_info.vertex_input_state = no_input;
|
||||
downscale_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||
downscale_pipe_info.target_info.num_color_targets = 1;
|
||||
downscale_pipe_info.target_info.color_target_descriptions = &downscale_color_target;
|
||||
|
||||
downscale_pipeline_ = SDL_CreateGPUGraphicsPipeline(device_, &downscale_pipe_info);
|
||||
|
||||
SDL_ReleaseGPUShader(device_, dvert);
|
||||
SDL_ReleaseGPUShader(device_, dfrag);
|
||||
|
||||
if (downscale_pipeline_ == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: downscale pipeline creation failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -707,8 +283,8 @@ namespace Rendering {
|
||||
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||
|
||||
#ifdef __APPLE__
|
||||
SDL_GPUShader* vert = createShaderMSL(device_, POSTFX_VERT_MSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderMSL(device_, CRTPI_FRAG_MSL, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
SDL_GPUShader* vert = createShaderMSL(device_, Rendering::Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderMSL(device_, Rendering::Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
#else
|
||||
SDL_GPUShader* vert = createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||
@@ -778,20 +354,6 @@ namespace Rendering {
|
||||
void SDL3GPUShader::render() { // NOLINT(readability-function-cognitive-complexity)
|
||||
if (!is_initialized_) { return; }
|
||||
|
||||
// Paso 0: si SS activo, calcular el factor necesario según el zoom actual y recrear si cambió.
|
||||
// Factor = primer múltiplo de 3 >= zoom (mín 3). Se recrea solo en saltos de factor.
|
||||
if (oversample_ > 1 && game_height_ > 0) {
|
||||
int win_w = 0;
|
||||
int win_h = 0;
|
||||
SDL_GetWindowSizeInPixels(window_, &win_w, &win_h);
|
||||
const float ZOOM = static_cast<float>(win_h) / static_cast<float>(game_height_);
|
||||
const int NEED_FACTOR = calcSsFactor(ZOOM);
|
||||
if (NEED_FACTOR != ss_factor_) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
recreateScaledTexture(NEED_FACTOR);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||
if (cmd == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
|
||||
@@ -847,46 +409,12 @@ namespace Rendering {
|
||||
source_height = game_height_ * internal_res_;
|
||||
}
|
||||
|
||||
// ---- Upscale pass: source_texture → scaled_texture_ ----
|
||||
// Si 4:3 actiu, l'estirament s'aplica ací directament (320x200 → W*factor × H*factor*1.2)
|
||||
// El filtre s'aplica sempre (texture_filter_linear_), independent de 4:3.
|
||||
// L'effective_scene/height reflecteix la textura real que veuen els shaders.
|
||||
// Sense SS ni stretch: scene_texture_ a game_height_.
|
||||
// Amb SS o stretch: scaled_texture_ a l'alçada escalada (amb o sense 4:3).
|
||||
// L'effective_scene és la textura font definitiva (internal_texture_
|
||||
// si internal_res_ > 1, altrament scene_texture_). effective_height
|
||||
// reflecteix l'alçada lògica del frame: game_height_ * 1.2 si 4:3 actiu.
|
||||
SDL_GPUTexture* effective_scene = source_texture;
|
||||
// `effective_height` reflecteix l'alçada lògica del frame (per a
|
||||
// scanlines i viewport), no la mida real de la textura. Es manté
|
||||
// a `game_height_` encara que internal_res_ > 1 — el multiplicador
|
||||
// només afecta la resolució física de la font, no l'aspect ni el
|
||||
// nombre de scanlines visibles.
|
||||
int effective_height = game_height_;
|
||||
(void)source_width; // només es fa servir com a context informatiu
|
||||
|
||||
if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) {
|
||||
SDL_GPUColorTargetInfo upscale_target = {};
|
||||
upscale_target.texture = scaled_texture_;
|
||||
upscale_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
|
||||
upscale_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
|
||||
// Filtre global: s'aplica sempre (ja no depèn de 4:3).
|
||||
bool use_linear = texture_filter_linear_;
|
||||
|
||||
SDL_GPURenderPass* upass = SDL_BeginGPURenderPass(cmd, &upscale_target, 1, nullptr);
|
||||
if (upass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
|
||||
SDL_GPUTextureSamplerBinding ubinding = {};
|
||||
ubinding.texture = source_texture;
|
||||
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
|
||||
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
|
||||
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(upass);
|
||||
}
|
||||
effective_scene = scaled_texture_;
|
||||
effective_height = stretch_4_3_ ? static_cast<int>(static_cast<float>(game_height_) * 1.2F) : game_height_;
|
||||
} else if (stretch_4_3_) {
|
||||
// Sense SS: el viewport s'encarrega de l'estirament geomètric
|
||||
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
|
||||
}
|
||||
int effective_height = stretch_4_3_ ? static_cast<int>(static_cast<float>(game_height_) * 1.2F) : game_height_;
|
||||
(void)source_width;
|
||||
(void)source_height;
|
||||
|
||||
// ---- Acquire swapchain texture ----
|
||||
@@ -948,19 +476,10 @@ namespace Rendering {
|
||||
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
||||
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
|
||||
|
||||
// pixel_scale: subpíxels per píxel lògic.
|
||||
// Sense SS: vh/effective_height (zoom de finestra).
|
||||
// Amb SS: ss_factor_ exacte (3, 6, 9...).
|
||||
if (oversample_ > 1 && ss_factor_ > 0) {
|
||||
uniforms_.pixel_scale = static_cast<float>(ss_factor_);
|
||||
} else {
|
||||
uniforms_.pixel_scale = (effective_height > 0) ? (vh / static_cast<float>(effective_height)) : 1.0F;
|
||||
}
|
||||
// pixel_scale: subpíxels per píxel lògic (zoom de finestra).
|
||||
uniforms_.pixel_scale = (effective_height > 0) ? (vh / static_cast<float>(effective_height)) : 1.0F;
|
||||
uniforms_.screen_height = static_cast<float>(effective_height);
|
||||
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||
uniforms_.oversample = (oversample_ > 1 && ss_factor_ > 0)
|
||||
? static_cast<float>(ss_factor_)
|
||||
: 1.0F;
|
||||
|
||||
// ---- Path CrtPi: directo scene_texture_ → swapchain, sin SS ni Lanczos ----
|
||||
if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) {
|
||||
@@ -999,87 +518,34 @@ namespace Rendering {
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Determinar si usar el path Lanczos (SS activo + algo seleccionado) ----
|
||||
const bool USE_LANCZOS = (oversample_ > 1 && downscale_algo_ > 0 && scaled_texture_ != nullptr && postfx_texture_ != nullptr && postfx_offscreen_pipeline_ != nullptr && downscale_pipeline_ != nullptr);
|
||||
// ---- Render pass: PostFX → swapchain ----
|
||||
// Font: effective_scene (que ja és internal_texture_ si internal_res_ > 1, o
|
||||
// scene_texture_ altrament). Sampler honora el filtre global de l'usuari.
|
||||
SDL_GPUColorTargetInfo color_target = {};
|
||||
color_target.texture = swapchain;
|
||||
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
|
||||
if (USE_LANCZOS) {
|
||||
// ---- Pass A: PostFX → postfx_texture_ (full scaled size, sin viewport) ----
|
||||
SDL_GPUColorTargetInfo postfx_target = {};
|
||||
postfx_target.texture = postfx_texture_;
|
||||
postfx_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
postfx_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
postfx_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
|
||||
if (pass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(pass, &vp);
|
||||
|
||||
SDL_GPURenderPass* ppass = SDL_BeginGPURenderPass(cmd, &postfx_target, 1, nullptr);
|
||||
if (ppass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(ppass, postfx_offscreen_pipeline_);
|
||||
SDL_GPUTextureSamplerBinding pbinding = {};
|
||||
pbinding.texture = scaled_texture_;
|
||||
pbinding.sampler = sampler_; // NEAREST: 1:1 pass, efectos calculados analíticamente
|
||||
SDL_BindGPUFragmentSamplers(ppass, 0, &pbinding, 1);
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
|
||||
SDL_DrawGPUPrimitives(ppass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(ppass);
|
||||
}
|
||||
SDL_GPUSampler* active_sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
|
||||
? linear_sampler_
|
||||
: sampler_;
|
||||
|
||||
// ---- Pass B: Downscale Lanczos → swapchain (con viewport/letterbox) ----
|
||||
SDL_GPUColorTargetInfo ds_target = {};
|
||||
ds_target.texture = swapchain;
|
||||
ds_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
ds_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
ds_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
SDL_GPUTextureSamplerBinding binding = {};
|
||||
binding.texture = effective_scene;
|
||||
binding.sampler = active_sampler;
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||
|
||||
SDL_GPURenderPass* dpass = SDL_BeginGPURenderPass(cmd, &ds_target, 1, nullptr);
|
||||
if (dpass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(dpass, downscale_pipeline_);
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(dpass, &vp);
|
||||
SDL_GPUTextureSamplerBinding dbinding = {};
|
||||
dbinding.texture = postfx_texture_;
|
||||
dbinding.sampler = sampler_; // NEAREST: el shader Lanczos hace su propia interpolación
|
||||
SDL_BindGPUFragmentSamplers(dpass, 0, &dbinding, 1);
|
||||
// algorithm: 0=Lanczos2, 1=Lanczos3 (downscale_algo_ es 1-based)
|
||||
DownscaleUniforms downscale_u = {.algorithm = downscale_algo_ - 1, .pad0 = 0.0F, .pad1 = 0.0F, .pad2 = 0.0F};
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &downscale_u, sizeof(DownscaleUniforms));
|
||||
SDL_DrawGPUPrimitives(dpass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(dpass);
|
||||
}
|
||||
} else {
|
||||
// ---- Render pass: PostFX → swapchain directamente (bilinear, comportamiento original) ----
|
||||
SDL_GPUColorTargetInfo color_target = {};
|
||||
color_target.texture = swapchain;
|
||||
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
|
||||
|
||||
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
|
||||
if (pass != nullptr) {
|
||||
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||
SDL_SetGPUViewport(pass, &vp);
|
||||
|
||||
// Font: amb SS scaled_texture_; sense SS, effective_scene (que ja
|
||||
// és internal_texture_ si internal_res_>1, o scene_texture_ si no).
|
||||
// Sampler: honora el filtre global que l'usuari tria al menú
|
||||
// (texture_filter_linear_). Abans estava hardcoded a NEAREST
|
||||
// quan SS era off — el menú no tenia efecte visible en aquest path.
|
||||
SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr)
|
||||
? scaled_texture_
|
||||
: effective_scene;
|
||||
SDL_GPUSampler* active_sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
|
||||
? linear_sampler_
|
||||
: sampler_;
|
||||
|
||||
SDL_GPUTextureSamplerBinding binding = {};
|
||||
binding.texture = input_texture;
|
||||
binding.sampler = active_sampler;
|
||||
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||
|
||||
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
|
||||
|
||||
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
}
|
||||
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||
SDL_EndGPURenderPass(pass);
|
||||
}
|
||||
|
||||
SDL_SubmitGPUCommandBuffer(cmd);
|
||||
@@ -1102,18 +568,10 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUGraphicsPipeline(device_, crtpi_pipeline_);
|
||||
crtpi_pipeline_ = nullptr;
|
||||
}
|
||||
if (postfx_offscreen_pipeline_ != nullptr) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(device_, postfx_offscreen_pipeline_);
|
||||
postfx_offscreen_pipeline_ = nullptr;
|
||||
}
|
||||
if (upscale_pipeline_ != nullptr) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(device_, upscale_pipeline_);
|
||||
upscale_pipeline_ = nullptr;
|
||||
}
|
||||
if (downscale_pipeline_ != nullptr) {
|
||||
SDL_ReleaseGPUGraphicsPipeline(device_, downscale_pipeline_);
|
||||
downscale_pipeline_ = nullptr;
|
||||
}
|
||||
if (scene_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||
scene_texture_ = nullptr;
|
||||
@@ -1122,15 +580,6 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUTexture(device_, internal_texture_);
|
||||
internal_texture_ = nullptr;
|
||||
}
|
||||
if (scaled_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
scaled_texture_ = nullptr;
|
||||
}
|
||||
if (postfx_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, postfx_texture_);
|
||||
postfx_texture_ = nullptr;
|
||||
}
|
||||
ss_factor_ = 0;
|
||||
if (upload_buffer_ != nullptr) {
|
||||
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||
upload_buffer_ = nullptr;
|
||||
@@ -1211,15 +660,17 @@ namespace Rendering {
|
||||
|
||||
void SDL3GPUShader::setPostFXParams(const PostFXParams& p) {
|
||||
uniforms_.vignette_strength = p.vignette;
|
||||
uniforms_.chroma_strength = p.chroma;
|
||||
uniforms_.chroma_min = p.chroma_min;
|
||||
uniforms_.chroma_max = p.chroma_max;
|
||||
uniforms_.mask_strength = p.mask;
|
||||
uniforms_.gamma_strength = p.gamma;
|
||||
uniforms_.curvature = p.curvature;
|
||||
uniforms_.bleeding = p.bleeding;
|
||||
uniforms_.flicker = p.flicker;
|
||||
|
||||
// Las scanlines siempre las aplica el shader PostFX en GPU.
|
||||
uniforms_.scanline_strength = p.scanlines;
|
||||
uniforms_.scan_dark_ratio = p.scan_dark_ratio;
|
||||
uniforms_.scan_dark_floor = p.scan_dark_floor;
|
||||
uniforms_.scan_edge_soft = p.scan_edge_soft;
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setCrtPiParams(const CrtPiParams& p) {
|
||||
@@ -1288,42 +739,11 @@ namespace Rendering {
|
||||
|
||||
void SDL3GPUShader::setStretch43(bool enabled) {
|
||||
stretch_4_3_ = enabled;
|
||||
if (!is_initialized_ || device_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recrear scaled_texture_ perquè tinga les dimensions correctes (amb o sense 4:3)
|
||||
if (oversample_ > 1 && ss_factor_ > 0) {
|
||||
SDL_WaitForGPUIdle(device_);
|
||||
recreateScaledTexture(ss_factor_);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// setOversample — cambia el factor SS; recrea texturas si ya está inicializado
|
||||
// ---------------------------------------------------------------------------
|
||||
void SDL3GPUShader::setOversample(int factor) {
|
||||
const int NEW_FACTOR = std::max(1, factor);
|
||||
if (NEW_FACTOR == oversample_) { return; }
|
||||
oversample_ = NEW_FACTOR;
|
||||
if (is_initialized_) {
|
||||
reinitTexturesAndBuffer();
|
||||
// scanline_strength se actualizará en el próximo setPostFXParams
|
||||
}
|
||||
}
|
||||
|
||||
void SDL3GPUShader::setDownscaleAlgo(int algo) {
|
||||
downscale_algo_ = std::max(0, std::min(algo, 2));
|
||||
}
|
||||
|
||||
auto SDL3GPUShader::getSsTextureSize() const -> std::pair<int, int> {
|
||||
if (ss_factor_ <= 1) { return {0, 0}; }
|
||||
return {game_width_ * ss_factor_, game_height_ * ss_factor_};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reinitTexturesAndBuffer — recrea scene_texture_, scaled_texture_ y
|
||||
// upload_buffer_ con el factor oversample_ actual. No toca pipelines ni samplers.
|
||||
// reinitTexturesAndBuffer — recrea scene_texture_, internal_texture_ i
|
||||
// upload_buffer_. No toca pipelines ni samplers.
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::reinitTexturesAndBuffer() -> bool {
|
||||
if (device_ == nullptr) { return false; }
|
||||
@@ -1337,22 +757,14 @@ namespace Rendering {
|
||||
SDL_ReleaseGPUTexture(device_, internal_texture_);
|
||||
internal_texture_ = nullptr;
|
||||
}
|
||||
// scaled_texture_ se libera aquí; se recreará en el primer render() con el factor correcto
|
||||
if (scaled_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
scaled_texture_ = nullptr;
|
||||
}
|
||||
ss_factor_ = 0;
|
||||
|
||||
if (upload_buffer_ != nullptr) {
|
||||
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||
upload_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
uniforms_.screen_height = static_cast<float>(game_height_);
|
||||
uniforms_.oversample = static_cast<float>(oversample_);
|
||||
|
||||
// scene_texture_: siempre a resolución del juego
|
||||
// scene_texture_: sempre a resolució del joc
|
||||
SDL_GPUTextureCreateInfo tex_info = {};
|
||||
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
tex_info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||
@@ -1367,7 +779,6 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
// upload_buffer_: siempre a resolución del juego
|
||||
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
|
||||
@@ -1379,82 +790,9 @@ namespace Rendering {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recrea la textura interna si internal_res_ > 1 — manté coherència
|
||||
// en canvis d'SS que passen per reinitTexturesAndBuffer().
|
||||
recreateInternalTexture();
|
||||
|
||||
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, SS %s, internal ×%d (scaled se creará en render)",
|
||||
game_width_,
|
||||
game_height_,
|
||||
oversample_ > 1 ? "on" : "off",
|
||||
internal_res_);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// calcSsFactor — primer múltiplo de 3 >= zoom, mínimo 3.
|
||||
// Ejemplos: zoom 1,2,3 → 3; zoom 4,5,6 → 6; zoom 4.4 → 6; zoom 7,8,9 → 9.
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::calcSsFactor(float zoom) -> int {
|
||||
const int MULTIPLE = 3;
|
||||
const int N = static_cast<int>(std::ceil(zoom / static_cast<float>(MULTIPLE)));
|
||||
return std::max(1, N) * MULTIPLE;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// recreateScaledTexture — libera y recrea scaled_texture_ para el factor dado.
|
||||
// Llamar solo cuando device_ no esté ejecutando comandos (SDL_WaitForGPUIdle previo).
|
||||
// ---------------------------------------------------------------------------
|
||||
auto SDL3GPUShader::recreateScaledTexture(int factor) -> bool {
|
||||
if (scaled_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
scaled_texture_ = nullptr;
|
||||
}
|
||||
if (postfx_texture_ != nullptr) {
|
||||
SDL_ReleaseGPUTexture(device_, postfx_texture_);
|
||||
postfx_texture_ = nullptr;
|
||||
}
|
||||
ss_factor_ = 0;
|
||||
|
||||
const int W = game_width_ * factor;
|
||||
// Si 4:3 actiu, l'alçada inclou l'estirament (200 * factor * 1.2)
|
||||
const int H = stretch_4_3_
|
||||
? static_cast<int>(static_cast<float>(game_height_) * 1.2F * static_cast<float>(factor))
|
||||
: game_height_ * factor;
|
||||
|
||||
SDL_GPUTextureCreateInfo info = {};
|
||||
info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||
info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
|
||||
info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
|
||||
info.width = static_cast<Uint32>(W);
|
||||
info.height = static_cast<Uint32>(H);
|
||||
info.layer_count_or_depth = 1;
|
||||
info.num_levels = 1;
|
||||
|
||||
scaled_texture_ = SDL_CreateGPUTexture(device_, &info);
|
||||
if (scaled_texture_ == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: failed to create scaled texture %dx%d (factor %d): %s",
|
||||
W,
|
||||
H,
|
||||
factor,
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
postfx_texture_ = SDL_CreateGPUTexture(device_, &info);
|
||||
if (postfx_texture_ == nullptr) {
|
||||
SDL_Log("SDL3GPUShader: failed to create postfx texture %dx%d (factor %d): %s",
|
||||
W,
|
||||
H,
|
||||
factor,
|
||||
SDL_GetError());
|
||||
SDL_ReleaseGPUTexture(device_, scaled_texture_);
|
||||
scaled_texture_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
ss_factor_ = factor;
|
||||
SDL_Log("SDL3GPUShader: scaled+postfx textures %dx%d (factor %d×)", W, H, factor);
|
||||
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, internal ×%d", game_width_, game_height_, internal_res_);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,20 +7,28 @@
|
||||
|
||||
// PostFX uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
|
||||
// 16 floats = 64 bytes (4 × vec4) — meets Metal/Vulkan 16-byte alignment.
|
||||
struct PostFXUniforms {
|
||||
// vec4 #0
|
||||
float vignette_strength; // 0 = none, ~0.8 = subtle
|
||||
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
|
||||
float chroma_min; // aberració cromàtica mínima (sempre present)
|
||||
float scanline_strength; // 0 = off, 1 = full
|
||||
float screen_height; // logical height in pixels (used by bleeding effect)
|
||||
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
|
||||
// vec4 #1
|
||||
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||
// vec4 #2
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||
float chroma_max; // si == chroma_min queda estàtic; si != pulsa sinusoidalment
|
||||
// vec4 #3 — paràmetres de forma de les scanlines (exposats per preset)
|
||||
float scan_dark_ratio; // fracció de subfila fosca (1/3 = 0.333 per defecte)
|
||||
float scan_dark_floor; // brillantor de la subfila fosca (0.42 per defecte)
|
||||
float scan_edge_soft; // suavitzat de la transició (0 = step dur, 1 = 1px físic)
|
||||
float pad3;
|
||||
};
|
||||
|
||||
// CrtPi uniforms pushed to fragment stage each frame.
|
||||
@@ -49,15 +57,6 @@ struct CrtPiUniforms {
|
||||
float texture_height; // Alto del canvas en píxeles (inyectado en render)
|
||||
};
|
||||
|
||||
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
|
||||
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
|
||||
struct DownscaleUniforms {
|
||||
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
|
||||
float pad0;
|
||||
float pad1;
|
||||
float pad2;
|
||||
};
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/**
|
||||
@@ -99,15 +98,6 @@ namespace Rendering {
|
||||
// Selecciona el mode de presentació lògica (DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER)
|
||||
void setScalingMode(Options::ScalingMode mode) override;
|
||||
|
||||
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
||||
void setOversample(int factor) override;
|
||||
|
||||
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setDownscaleAlgo(int algo) override;
|
||||
|
||||
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
|
||||
|
||||
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
||||
void setActiveShader(ShaderType type) override;
|
||||
|
||||
@@ -146,39 +136,30 @@ namespace Rendering {
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||
|
||||
auto createPipeline() -> bool;
|
||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
|
||||
auto recreateInternalTexture() -> bool; // Recrea internal_texture_ (res interna × N)
|
||||
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
|
||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
auto recreateInternalTexture() -> bool; // Recrea internal_texture_ (res interna × N)
|
||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
|
||||
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
|
||||
SDL_GPUTexture* internal_texture_ = nullptr; // Resolució interna ampliada (game·N × game·N), si N>1
|
||||
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor, amb 4:3 si actiu)
|
||||
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolució escalada, sols amb Lanczos
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass → swapchain
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass → swapchain
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale per al pas de resolució interna
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
|
||||
SDL_GPUTexture* internal_texture_ = nullptr; // Resolució interna ampliada (game·N × game·N), si N>1
|
||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR (per texture_filter_linear_)
|
||||
|
||||
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 200.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
|
||||
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_min = 0.15F, .scanline_strength = 0.7F, .screen_height = 200.0F, .pixel_scale = 1.0F, .chroma_max = 0.15F, .scan_dark_ratio = 0.333F, .scan_dark_floor = 0.42F, .scan_edge_soft = 1.0F};
|
||||
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
|
||||
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
|
||||
|
||||
int game_width_ = 0; // Dimensions originals del canvas
|
||||
int game_height_ = 0;
|
||||
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
|
||||
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
||||
int internal_res_ = 1; // Multiplicador de resolució interna (1 = off)
|
||||
int internal_res_ = 1; // Multiplicador de resolució interna (1 = off)
|
||||
std::string driver_name_;
|
||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||
bool is_initialized_ = false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -21,12 +21,19 @@ namespace Rendering {
|
||||
struct PostFXParams {
|
||||
float vignette = 0.0F; // Intensidad de la viñeta
|
||||
float scanlines = 0.0F; // Intensidad de las scanlines
|
||||
float chroma = 0.0F; // Aberración cromática
|
||||
// Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
|
||||
// queda estàtica. min > 0 garanteix que la imatge mai sigui lliure de chroma.
|
||||
float chroma_min = 0.0F;
|
||||
float chroma_max = 0.0F;
|
||||
float mask = 0.0F; // Máscara de fósforo RGB
|
||||
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
|
||||
float curvature = 0.0F; // Curvatura barrel CRT
|
||||
float bleeding = 0.0F; // Sangrado de color NTSC
|
||||
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
|
||||
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
|
||||
float scan_dark_ratio = 0.333F; // fracció obscura (1/3)
|
||||
float scan_dark_floor = 0.42F; // brillantor subfila fosca
|
||||
float scan_edge_soft = 1.0F; // 0 = step dur; 1 = suavitzat 1 px físic
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -112,27 +119,6 @@ namespace Rendering {
|
||||
*/
|
||||
virtual void setScalingMode(Options::ScalingMode /*mode*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
||||
* Con factor > 1, la textura GPU se crea a game×factor resolución y
|
||||
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
|
||||
*/
|
||||
virtual void setOversample(int /*factor*/) {}
|
||||
|
||||
/**
|
||||
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
|
||||
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
|
||||
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
|
||||
*/
|
||||
virtual void setDownscaleAlgo(int /*algo*/) {}
|
||||
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
|
||||
|
||||
/**
|
||||
* @brief Devuelve las dimensiones de la textura de supersampling.
|
||||
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
|
||||
|
||||
/**
|
||||
* @brief Verifica si el backend está usando aceleración por hardware
|
||||
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
|
||||
|
||||
@@ -16,10 +16,8 @@ namespace Defaults::KeysGame {
|
||||
namespace Defaults::Video {
|
||||
constexpr bool GPU_ACCELERATION = true;
|
||||
constexpr bool SHADER_ENABLED = false;
|
||||
constexpr bool SUPERSAMPLING = false;
|
||||
constexpr bool VSYNC = true;
|
||||
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
|
||||
constexpr int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
|
||||
constexpr int INTERNAL_RESOLUTION = 1; // Multiplicador enter de la textura font abans del pipeline
|
||||
// TextureFilter i ScalingMode viuen a Options (requereixen #include, evitem dependència circular).
|
||||
} // namespace Defaults::Video
|
||||
|
||||
+21
-23
@@ -145,9 +145,6 @@ namespace Options {
|
||||
if (node.contains("shader_enabled")) {
|
||||
video.shader_enabled = node["shader_enabled"].get_value<bool>();
|
||||
}
|
||||
if (node.contains("supersampling")) {
|
||||
video.supersampling = node["supersampling"].get_value<bool>();
|
||||
}
|
||||
if (node.contains("scaling_mode")) {
|
||||
auto s = node["scaling_mode"].get_value<std::string>();
|
||||
if (s == "disabled") {
|
||||
@@ -172,9 +169,6 @@ namespace Options {
|
||||
auto s = node["texture_filter"].get_value<std::string>();
|
||||
video.texture_filter = (s == "linear") ? TextureFilter::LINEAR : TextureFilter::NEAREST;
|
||||
}
|
||||
if (node.contains("downscale_algo")) {
|
||||
video.downscale_algo = node["downscale_algo"].get_value<int>();
|
||||
}
|
||||
if (node.contains("internal_resolution")) {
|
||||
video.internal_resolution = node["internal_resolution"].get_value<int>();
|
||||
video.internal_resolution = std::max(video.internal_resolution, 1);
|
||||
@@ -344,7 +338,6 @@ namespace Options {
|
||||
file << "video:\n";
|
||||
file << " gpu_acceleration: " << (video.gpu_acceleration ? "true" : "false") << "\n";
|
||||
file << " shader_enabled: " << (video.shader_enabled ? "true" : "false") << "\n";
|
||||
file << " supersampling: " << (video.supersampling ? "true" : "false") << "\n";
|
||||
{
|
||||
const char* m = nullptr;
|
||||
switch (video.scaling_mode) {
|
||||
@@ -370,7 +363,6 @@ namespace Options {
|
||||
file << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
||||
file << " aspect_ratio_4_3: " << (video.aspect_ratio_4_3 ? "true" : "false") << "\n";
|
||||
file << " texture_filter: " << (video.texture_filter == TextureFilter::LINEAR ? "linear" : "nearest") << " # nearest|linear\n";
|
||||
file << " downscale_algo: " << video.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
|
||||
file << " internal_resolution: " << video.internal_resolution << " # multiplicador enter font, clampat a max_zoom\n";
|
||||
file << " current_shader: " << video.current_shader << "\n";
|
||||
file << " current_postfx_preset: " << video.current_postfx_preset << "\n";
|
||||
@@ -481,22 +473,24 @@ namespace Options {
|
||||
// Escriure defaults
|
||||
std::ofstream out(postfx_file_path);
|
||||
if (out.is_open()) {
|
||||
out << "# Aventures En Egipte - PostFX Shader Presets\n\n";
|
||||
out << "# Aventures En Egipte - PostFX Shader Presets\n";
|
||||
out << "# chroma_min/max: min == max → estàtic; min != max → pulsa sinusoïdalment.\n";
|
||||
out << "# scan_dark_ratio/floor/edge_soft: forma analítica de les scanlines.\n\n";
|
||||
out << "presets:\n";
|
||||
out << " - name: \"CRT\"\n vignette: 0.6\n scanlines: 0.7\n chroma: 0.15\n mask: 0.6\n gamma: 0.8\n";
|
||||
out << " - name: \"NTSC\"\n vignette: 0.4\n scanlines: 0.5\n chroma: 0.2\n mask: 0.4\n gamma: 0.5\n bleeding: 0.6\n";
|
||||
out << " - name: \"CURVED\"\n vignette: 0.5\n scanlines: 0.6\n chroma: 0.1\n mask: 0.5\n gamma: 0.7\n curvature: 0.8\n";
|
||||
out << " - name: \"SCANLINES\"\n scanlines: 0.8\n vignette: 0.0\n chroma: 0.0\n";
|
||||
out << " - name: \"SUBTLE\"\n vignette: 0.3\n scanlines: 0.4\n chroma: 0.05\n gamma: 0.3\n";
|
||||
out << " - name: \"CRT LIVE\"\n vignette: 0.5\n scanlines: 0.6\n chroma: 0.3\n mask: 0.3\n gamma: 0.4\n curvature: 0.3\n bleeding: 0.4\n flicker: 0.8\n";
|
||||
out << " - name: \"CRT\"\n vignette: 0.6\n scanlines: 0.7\n chroma_min: 0.15\n chroma_max: 0.15\n mask: 0.6\n gamma: 0.8\n";
|
||||
out << " - name: \"NTSC\"\n vignette: 0.4\n scanlines: 0.5\n chroma_min: 0.2\n chroma_max: 0.2\n mask: 0.4\n gamma: 0.5\n bleeding: 0.6\n";
|
||||
out << " - name: \"CURVED\"\n vignette: 0.5\n scanlines: 0.6\n chroma_min: 0.1\n chroma_max: 0.1\n mask: 0.5\n gamma: 0.7\n curvature: 0.8\n";
|
||||
out << " - name: \"SCANLINES\"\n scanlines: 0.8\n vignette: 0.0\n chroma_min: 0.0\n chroma_max: 0.0\n";
|
||||
out << " - name: \"SUBTLE\"\n vignette: 0.3\n scanlines: 0.4\n chroma_min: 0.05\n chroma_max: 0.05\n gamma: 0.3\n";
|
||||
out << " - name: \"CRT LIVE\"\n vignette: 0.5\n scanlines: 0.6\n chroma_min: 0.3\n chroma_max: 0.3\n mask: 0.3\n gamma: 0.4\n curvature: 0.3\n bleeding: 0.4\n flicker: 0.8\n";
|
||||
out.close();
|
||||
}
|
||||
postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F, 0.6F, 0.8F});
|
||||
postfx_presets.push_back({"NTSC", 0.4F, 0.5F, 0.2F, 0.4F, 0.5F, 0.0F, 0.6F});
|
||||
postfx_presets.push_back({"CURVED", 0.5F, 0.6F, 0.1F, 0.5F, 0.7F, 0.8F});
|
||||
postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F});
|
||||
postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.3F});
|
||||
postfx_presets.push_back({"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F});
|
||||
postfx_presets.push_back({.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma_min = 0.15F, .chroma_max = 0.15F, .mask = 0.6F, .gamma = 0.8F});
|
||||
postfx_presets.push_back({.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma_min = 0.2F, .chroma_max = 0.2F, .mask = 0.4F, .gamma = 0.5F, .bleeding = 0.6F});
|
||||
postfx_presets.push_back({.name = "CURVED", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.1F, .chroma_max = 0.1F, .mask = 0.5F, .gamma = 0.7F, .curvature = 0.8F});
|
||||
postfx_presets.push_back({.name = "SCANLINES", .vignette = 0.0F, .scanlines = 0.8F, .chroma_min = 0.0F, .chroma_max = 0.0F});
|
||||
postfx_presets.push_back({.name = "SUBTLE", .vignette = 0.3F, .scanlines = 0.4F, .chroma_min = 0.05F, .chroma_max = 0.05F, .gamma = 0.3F});
|
||||
postfx_presets.push_back({.name = "CRT LIVE", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.3F, .chroma_max = 0.3F, .mask = 0.3F, .gamma = 0.4F, .curvature = 0.3F, .bleeding = 0.4F, .flicker = 0.8F});
|
||||
current_postfx_preset = 0;
|
||||
return true;
|
||||
}
|
||||
@@ -514,12 +508,16 @@ namespace Options {
|
||||
}
|
||||
parseFloatField(p, "vignette", preset.vignette);
|
||||
parseFloatField(p, "scanlines", preset.scanlines);
|
||||
parseFloatField(p, "chroma", preset.chroma);
|
||||
parseFloatField(p, "chroma_min", preset.chroma_min);
|
||||
parseFloatField(p, "chroma_max", preset.chroma_max);
|
||||
parseFloatField(p, "mask", preset.mask);
|
||||
parseFloatField(p, "gamma", preset.gamma);
|
||||
parseFloatField(p, "curvature", preset.curvature);
|
||||
parseFloatField(p, "bleeding", preset.bleeding);
|
||||
parseFloatField(p, "flicker", preset.flicker);
|
||||
parseFloatField(p, "scan_dark_ratio", preset.scan_dark_ratio);
|
||||
parseFloatField(p, "scan_dark_floor", preset.scan_dark_floor);
|
||||
parseFloatField(p, "scan_edge_soft", preset.scan_edge_soft);
|
||||
postfx_presets.push_back(preset);
|
||||
}
|
||||
}
|
||||
@@ -528,7 +526,7 @@ namespace Options {
|
||||
return true;
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Error parsing PostFX YAML: " << e.what() << '\n';
|
||||
postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F, 0.6F, 0.8F});
|
||||
postfx_presets.push_back({.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma_min = 0.15F, .chroma_max = 0.15F, .mask = 0.6F, .gamma = 0.8F});
|
||||
current_postfx_preset = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -40,12 +40,10 @@ namespace Options {
|
||||
struct Video {
|
||||
bool gpu_acceleration{Defaults::Video::GPU_ACCELERATION};
|
||||
bool shader_enabled{Defaults::Video::SHADER_ENABLED};
|
||||
bool supersampling{Defaults::Video::SUPERSAMPLING};
|
||||
ScalingMode scaling_mode{ScalingMode::INTEGER};
|
||||
bool vsync{Defaults::Video::VSYNC};
|
||||
bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3};
|
||||
TextureFilter texture_filter{TextureFilter::NEAREST};
|
||||
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
|
||||
int internal_resolution{Defaults::Video::INTERNAL_RESOLUTION}; // Multiplicador enter ≥ 1, clampat a max_zoom
|
||||
std::string current_shader{"postfx"}; // "postfx" o "crtpi"
|
||||
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
|
||||
@@ -99,12 +97,18 @@ namespace Options {
|
||||
std::string name;
|
||||
float vignette{0.6F};
|
||||
float scanlines{0.7F};
|
||||
float chroma{0.15F};
|
||||
// Aberració cromàtica: min == max → estàtic; min != max → pulsa sinusoïdalment.
|
||||
float chroma_min{0.15F};
|
||||
float chroma_max{0.15F};
|
||||
float mask{0.0F};
|
||||
float gamma{0.0F};
|
||||
float curvature{0.0F};
|
||||
float bleeding{0.0F};
|
||||
float flicker{0.0F};
|
||||
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
|
||||
float scan_dark_ratio{0.333F};
|
||||
float scan_dark_floor{0.42F};
|
||||
float scan_edge_soft{1.0F};
|
||||
};
|
||||
|
||||
// Preset CrtPi
|
||||
|
||||
@@ -43,10 +43,10 @@ namespace Scenes {
|
||||
private:
|
||||
enum class Phase : std::uint8_t {
|
||||
INITIAL_WAIT, // 1000 ms pantalla negra
|
||||
REVEAL, // 15 passos del wordmark
|
||||
REVEAL, // 15 passos del wordmark
|
||||
PALETTE_CYCLE, // 256 × 20 ms mutant pal[16..31]
|
||||
FINAL_WAIT, // 200 ms abans de la sub-escena de sprites
|
||||
SPRITES, // tick delegat a IntroSpritesScene fins que acaba
|
||||
SPRITES, // tick delegat a IntroSpritesScene fins que acaba
|
||||
DONE,
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ set(SHADER_SOURCES
|
||||
"postfx.vert"
|
||||
"postfx.frag"
|
||||
"upscale.frag"
|
||||
"downscale.frag"
|
||||
"crtpi_frag.glsl"
|
||||
)
|
||||
|
||||
@@ -25,7 +24,6 @@ set(SHADER_VARS
|
||||
"kpostfx_vert_spv"
|
||||
"kpostfx_frag_spv"
|
||||
"kupscale_frag_spv"
|
||||
"kdownscale_frag_spv"
|
||||
"kcrtpi_frag_spv"
|
||||
)
|
||||
|
||||
@@ -34,7 +32,6 @@ set(SHADER_FLAGS
|
||||
""
|
||||
""
|
||||
""
|
||||
""
|
||||
"-fshader-stage=frag"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user