Compare commits

2 Commits

Author SHA1 Message Date
c87779cc09 imlementant supersampling 2026-03-22 21:24:20 +01:00
24594fa89a deixant postfx al gust 2026-03-22 20:54:02 +01:00
7 changed files with 286 additions and 73 deletions

View File

@@ -22,6 +22,10 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
float gamma_strength; float gamma_strength;
float curvature; float curvature;
float bleeding; float bleeding;
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init (for future animated effects)
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
float pad1; // padding — 48 bytes total (3 × 16)
} u; } u;
// YCbCr helpers for NTSC bleeding // YCbCr helpers for NTSC bleeding
@@ -64,15 +68,17 @@ void main() {
// Muestra base // Muestra base
vec3 base = texture(scene, uv).rgb; vec3 base = texture(scene, uv).rgb;
// Sangrado NTSC — difuminado horizontal de crominancia // Sangrado NTSC — difuminado horizontal de crominancia.
// step = 1 pixel lógico de juego en UV (corrige SS: textureSize.x = game_w * oversample).
vec3 colour; vec3 colour;
if (u.bleeding > 0.0) { if (u.bleeding > 0.0) {
float tw = float(textureSize(scene, 0).x); float tw = float(textureSize(scene, 0).x);
float step = u.oversample / tw; // 1 pixel lógico en UV
vec3 ycc = rgb_to_ycc(base); vec3 ycc = rgb_to_ycc(base);
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0/tw, 0.0)).rgb); 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/tw, 0.0)).rgb); vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0/tw, 0.0)).rgb); vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0*step, 0.0)).rgb);
vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0/tw, 0.0)).rgb); vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0*step, 0.0)).rgb);
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0; ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0;
colour = mix(base, ycc_to_rgb(ycc), u.bleeding); colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
} else { } else {
@@ -90,14 +96,19 @@ void main() {
colour = mix(colour, lin, u.gamma_strength); colour = mix(colour, lin, u.gamma_strength);
} }
// Scanlines // Scanlines — 1 pixel físico oscuro por fila lógica.
float texHeight = float(textureSize(scene, 0).y); // Usa uv.y (independiente del offset de letterbox) con pixel_scale para
float scaleY = u.screen_height / texHeight; // calcular la posición dentro de la fila en coordenadas físicas.
float screenY = uv.y * u.screen_height; // 3x: 1 dark + 2 bright. 4x: 1 dark + 3 bright.
float posInRow = mod(screenY, scaleY); // bright=3.5×, dark floor=0.42 (mantiene aspecto CRT original).
float scanLineDY = posInRow / scaleY - 0.5; if (u.scanline_strength > 0.0) {
float scan = max(1.0 - scanLineDY * scanLineDY * 6.0, 0.12) * 3.5; 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);
float is_dark = step(ps - 1.0, row_pos);
float scan = mix(3.5, 0.42, is_dark);
colour *= mix(1.0, scan, u.scanline_strength); colour *= mix(1.0, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0) { if (u.gamma_strength > 0.0) {
vec3 enc = pow(colour, vec3(1.0 / 2.2)); vec3 enc = pow(colour, vec3(1.0 / 2.2));
@@ -109,7 +120,8 @@ void main() {
float vignette = 1.0 - dot(d, d) * u.vignette_strength; float vignette = 1.0 - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0, 1.0); colour *= clamp(vignette, 0.0, 1.0);
// Máscara de fósforo RGB // 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.0) { if (u.mask_strength > 0.0) {
float whichMask = fract(gl_FragCoord.x * 0.3333333); float whichMask = fract(gl_FragCoord.x * 0.3333333);
vec3 mask = vec3(0.80); vec3 mask = vec3(0.80);

View File

@@ -463,7 +463,10 @@ auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
void Screen::applyCurrentPostFXPreset() { void Screen::applyCurrentPostFXPreset() {
if (shader_backend_ && !Options::postfx_presets.empty()) { if (shader_backend_ && !Options::postfx_presets.empty()) {
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)]; const auto& p = Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)];
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding}; // setOversample primero: puede recrear texturas y debe conocer el factor
// antes de que setPostFXParams decida si hornear scanlines en CPU o GPU.
shader_backend_->setOversample(p.supersampling ? 3 : 1);
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .supersampling = p.supersampling};
shader_backend_->setPostFXParams(params); shader_backend_->setPostFXParams(params);
} }
} }

View File

@@ -54,6 +54,10 @@ struct PostFXUniforms {
float gamma_strength; float gamma_strength;
float curvature; float curvature;
float bleeding; float bleeding;
float pixel_scale;
float time;
float oversample; // 1.0 = sin SS, 3.0 = 3× supersampling
float pad1;
}; };
// YCbCr helpers for NTSC bleeding // YCbCr helpers for NTSC bleeding
@@ -98,15 +102,17 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
// Muestra base // Muestra base
float3 base = scene.sample(samp, uv).rgb; float3 base = scene.sample(samp, uv).rgb;
// Sangrado NTSC — difuminado horizontal de crominancia // Sangrado NTSC — difuminado horizontal de crominancia.
// step = 1 pixel de juego en espacio UV (corrige SS: scene.get_width() = game_w * oversample).
float3 colour; float3 colour;
if (u.bleeding > 0.0f) { if (u.bleeding > 0.0f) {
float tw = float(scene.get_width()); 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 = rgb_to_ycc(base);
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f/tw, 0.0f)).rgb); 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/tw, 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/tw, 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/tw, 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; 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); colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
} else { } else {
@@ -124,14 +130,19 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
colour = mix(colour, lin, u.gamma_strength); colour = mix(colour, lin, u.gamma_strength);
} }
// Scanlines // Scanlines — 1 pixel físico oscuro por fila lógica.
float texHeight = float(scene.get_height()); // Usa uv.y (independiente del offset de letterbox) con pixel_scale para
float scaleY = u.screen_height / texHeight; // calcular la posición dentro de la fila en coordenadas físicas.
float screenY = uv.y * u.screen_height; // 3x: 1 dark + 2 bright. 4x: 1 dark + 3 bright.
float posInRow = fmod(screenY, scaleY); // bright=3.5×, dark floor=0.42 (mantiene aspecto CRT original).
float scanLineDY = posInRow / scaleY - 0.5f; if (u.scanline_strength > 0.0f) {
float scan = max(1.0f - scanLineDY * scanLineDY * 6.0f, 0.12f) * 3.5f; 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 is_dark = step(ps - 1.0f, row_pos);
float scan = mix(3.5f, 0.42f, is_dark);
colour *= mix(1.0f, scan, u.scanline_strength); colour *= mix(1.0f, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0f) { if (u.gamma_strength > 0.0f) {
float3 enc = pow(colour, float3(1.0f/2.2f)); float3 enc = pow(colour, float3(1.0f/2.2f));
@@ -143,7 +154,8 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]],
float vignette = 1.0f - dot(d, d) * u.vignette_strength; float vignette = 1.0f - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0f, 1.0f); colour *= clamp(vignette, 0.0f, 1.0f);
// Máscara de fósforo RGB // 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) { if (u.mask_strength > 0.0f) {
float whichMask = fract(in.pos.x * 0.3333333f); float whichMask = fract(in.pos.x * 0.3333333f);
float3 mask = float3(0.80f); float3 mask = float3(0.80f);
@@ -189,9 +201,12 @@ namespace Rendering {
float fw = 0.0F; float fw = 0.0F;
float fh = 0.0F; float fh = 0.0F;
SDL_GetTextureSize(texture, &fw, &fh); SDL_GetTextureSize(texture, &fw, &fh);
tex_width_ = static_cast<int>(fw); game_width_ = static_cast<int>(fw);
tex_height_ = static_cast<int>(fh); game_height_ = static_cast<int>(fh);
uniforms_.screen_height = fh; // Altura lógica del juego (no el swapchain físico) tex_width_ = game_width_ * oversample_;
tex_height_ = game_height_ * oversample_;
uniforms_.screen_height = static_cast<float>(tex_height_); // Altura de la textura GPU
uniforms_.oversample = static_cast<float>(oversample_);
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// 1. Create GPU device (solo si no existe ya) // 1. Create GPU device (solo si no existe ya)
@@ -254,7 +269,7 @@ namespace Rendering {
} }
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// 5. Create nearest-neighbour sampler (retro pixel art) // 5. Create samplers: NEAREST (pixel art) + LINEAR (supersampling)
// ---------------------------------------------------------------- // ----------------------------------------------------------------
SDL_GPUSamplerCreateInfo samp_info = {}; SDL_GPUSamplerCreateInfo samp_info = {};
samp_info.min_filter = SDL_GPU_FILTER_NEAREST; samp_info.min_filter = SDL_GPU_FILTER_NEAREST;
@@ -270,6 +285,20 @@ namespace Rendering {
return false; return false;
} }
SDL_GPUSamplerCreateInfo lsamp_info = {};
lsamp_info.min_filter = SDL_GPU_FILTER_LINEAR;
lsamp_info.mag_filter = SDL_GPU_FILTER_LINEAR;
lsamp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
lsamp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
lsamp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
lsamp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
linear_sampler_ = SDL_CreateGPUSampler(device_, &lsamp_info);
if (linear_sampler_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create linear sampler: %s", SDL_GetError());
cleanup();
return false;
}
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// 6. Create PostFX graphics pipeline // 6. Create PostFX graphics pipeline
// ---------------------------------------------------------------- // ----------------------------------------------------------------
@@ -335,7 +364,9 @@ namespace Rendering {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// uploadPixels — copies ARGB8888 CPU pixels into the GPU transfer buffer // uploadPixels — copies ARGB8888 CPU pixels into the GPU transfer buffer.
// Con supersampling (oversample_ > 1) expande cada pixel del juego a un bloque
// oversample × oversample y hornea la scanline oscura en la última fila del bloque.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void SDL3GPUShader::uploadPixels(const Uint32* pixels, int width, int height) { void SDL3GPUShader::uploadPixels(const Uint32* pixels, int width, int height) {
if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; } if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; }
@@ -345,7 +376,45 @@ namespace Rendering {
SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError()); SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError());
return; return;
} }
if (oversample_ <= 1) {
// Path sin supersampling: copia directa
std::memcpy(mapped, pixels, static_cast<size_t>(width * height * 4)); std::memcpy(mapped, pixels, static_cast<size_t>(width * height * 4));
} else {
// Path con supersampling: expande cada pixel a OS×OS, oscurece última fila.
// Replica la fórmula del shader: mix(3.5, 0.42, scanline_strength).
auto* out = static_cast<Uint32*>(mapped);
const int OS = oversample_;
const float BRIGHT_MUL = 1.0F + (baked_scanline_strength_ * 2.5F); // rows 0..OS-2
const float DARK_MUL = 1.0F - (baked_scanline_strength_ * 0.58F); // row OS-1
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const Uint32 SRC = pixels[y * width + x];
const Uint32 ALPHA = (SRC >> 24) & 0xFFU;
const auto FR = static_cast<float>((SRC >> 16) & 0xFFU);
const auto FG = static_cast<float>((SRC >> 8) & 0xFFU);
const auto FB = static_cast<float>( SRC & 0xFFU);
auto make_px = [ALPHA](float rv, float gv, float bv) -> Uint32 {
auto cl = [](float v) -> Uint32 { return static_cast<Uint32>(std::min(255.0F, v)); };
return (ALPHA << 24) | (cl(rv) << 16) | (cl(gv) << 8) | cl(bv);
};
const Uint32 BRIGHT = make_px(FR * BRIGHT_MUL, FG * BRIGHT_MUL, FB * BRIGHT_MUL);
const Uint32 DARK = make_px(FR * DARK_MUL, FG * DARK_MUL, FB * DARK_MUL);
for (int dy = 0; dy < OS; ++dy) {
const Uint32 OUT_PX = (dy == OS - 1) ? DARK : BRIGHT;
const int DST_Y = (y * OS) + dy;
for (int dx = 0; dx < OS; ++dx) {
out[DST_Y * (width * OS) + (x * OS) + dx] = OUT_PX;
}
}
}
}
}
SDL_UnmapGPUTransferBuffer(device_, upload_buffer_); SDL_UnmapGPUTransferBuffer(device_, upload_buffer_);
} }
@@ -406,30 +475,45 @@ namespace Rendering {
if (pass != nullptr) { if (pass != nullptr) {
SDL_BindGPUGraphicsPipeline(pass, pipeline_); SDL_BindGPUGraphicsPipeline(pass, pipeline_);
// Calcular viewport para mantener relación de aspecto (letterbox o integer scale) // Calcular viewport usando las dimensiones lógicas del canvas (game_width_/height_),
// no las de la textura GPU (que pueden ser game×3 con supersampling).
// El GPU escala la textura para cubrir el viewport independientemente de su resolución.
float vx = 0.0F; float vx = 0.0F;
float vy = 0.0F; float vy = 0.0F;
float vw = 0.0F; float vw = 0.0F;
float vh = 0.0F; float vh = 0.0F;
if (integer_scale_) { if (integer_scale_) {
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / tex_width_, static_cast<int>(sh) / tex_height_)); const int SCALE = std::max(1, std::min(static_cast<int>(sw) / game_width_, static_cast<int>(sh) / game_height_));
vw = static_cast<float>(tex_width_ * SCALE); vw = static_cast<float>(game_width_ * SCALE);
vh = static_cast<float>(tex_height_ * SCALE); vh = static_cast<float>(game_height_ * SCALE);
} else { } else {
const float SCALE = std::min( const float SCALE = std::min(
static_cast<float>(sw) / static_cast<float>(tex_width_), static_cast<float>(sw) / static_cast<float>(game_width_),
static_cast<float>(sh) / static_cast<float>(tex_height_)); static_cast<float>(sh) / static_cast<float>(game_height_));
vw = static_cast<float>(tex_width_) * SCALE; vw = static_cast<float>(game_width_) * SCALE;
vh = static_cast<float>(tex_height_) * SCALE; vh = static_cast<float>(game_height_) * SCALE;
} }
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F); vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F); vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
SDL_GPUViewport vp = {vx, vy, vw, vh, 0.0F, 1.0F}; SDL_GPUViewport vp = {vx, vy, vw, vh, 0.0F, 1.0F};
SDL_SetGPUViewport(pass, &vp); SDL_SetGPUViewport(pass, &vp);
// pixel_scale: pixels físicos por pixel lógico de juego (para scanlines sin SS).
// Con SS las scanlines están horneadas en CPU → scanline_strength=0 → no se usa.
uniforms_.pixel_scale = (game_height_ > 0)
? (vh / static_cast<float>(game_height_))
: 1.0F;
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
uniforms_.oversample = static_cast<float>(oversample_);
// Con supersampling usamos LINEAR para que el escalado a zooms no-múltiplo-de-3
// promedia correctamente las filas de scanline horneadas en CPU.
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr)
? linear_sampler_ : sampler_;
SDL_GPUTextureSamplerBinding binding = {}; SDL_GPUTextureSamplerBinding binding = {};
binding.texture = scene_texture_; binding.texture = scene_texture_;
binding.sampler = sampler_; binding.sampler = active_sampler;
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms)); SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
@@ -466,6 +550,10 @@ namespace Rendering {
SDL_ReleaseGPUSampler(device_, sampler_); SDL_ReleaseGPUSampler(device_, sampler_);
sampler_ = nullptr; sampler_ = nullptr;
} }
if (linear_sampler_ != nullptr) {
SDL_ReleaseGPUSampler(device_, linear_sampler_);
linear_sampler_ = nullptr;
}
// device_ y el claim de la ventana se mantienen vivos // device_ y el claim de la ventana se mantienen vivos
} }
} }
@@ -534,12 +622,16 @@ namespace Rendering {
void SDL3GPUShader::setPostFXParams(const PostFXParams& p) { void SDL3GPUShader::setPostFXParams(const PostFXParams& p) {
uniforms_.vignette_strength = p.vignette; uniforms_.vignette_strength = p.vignette;
uniforms_.scanline_strength = p.scanlines;
uniforms_.chroma_strength = p.chroma; uniforms_.chroma_strength = p.chroma;
uniforms_.mask_strength = p.mask; uniforms_.mask_strength = p.mask;
uniforms_.gamma_strength = p.gamma; uniforms_.gamma_strength = p.gamma;
uniforms_.curvature = p.curvature; uniforms_.curvature = p.curvature;
uniforms_.bleeding = p.bleeding; uniforms_.bleeding = p.bleeding;
// Con supersampling las scanlines se hornean en CPU (uploadPixels).
// El shader recibe strength=0 para no aplicarlas de nuevo en GPU.
baked_scanline_strength_ = p.scanlines;
uniforms_.scanline_strength = (oversample_ > 1) ? 0.0F : p.scanlines;
} }
void SDL3GPUShader::setVSync(bool vsync) { void SDL3GPUShader::setVSync(bool vsync) {
@@ -553,4 +645,68 @@ namespace Rendering {
integer_scale_ = integer_scale; integer_scale_ = integer_scale;
} }
// ---------------------------------------------------------------------------
// 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
}
}
// ---------------------------------------------------------------------------
// reinitTexturesAndBuffer — recrea scene_texture_ y upload_buffer_ con el
// tamaño actual (game × oversample_). No toca pipeline ni samplers.
// ---------------------------------------------------------------------------
auto SDL3GPUShader::reinitTexturesAndBuffer() -> bool {
if (device_ == nullptr) { return false; }
SDL_WaitForGPUIdle(device_);
if (scene_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (upload_buffer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
upload_buffer_ = nullptr;
}
tex_width_ = game_width_ * oversample_;
tex_height_ = game_height_ * oversample_;
uniforms_.screen_height = static_cast<float>(tex_height_);
uniforms_.oversample = static_cast<float>(oversample_);
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<Uint32>(tex_width_);
tex_info.height = static_cast<Uint32>(tex_height_);
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create scene texture: %s", SDL_GetError());
return false;
}
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = static_cast<Uint32>(tex_width_ * tex_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create upload buffer: %s", SDL_GetError());
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
return false;
}
SDL_Log("SDL3GPUShader: oversample %d → texture %dx%d", oversample_, tex_width_, tex_height_);
return true;
}
} // namespace Rendering } // namespace Rendering

View File

@@ -7,16 +7,20 @@
// PostFX uniforms pushed to fragment stage each frame. // PostFX uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout. // Must match the MSL struct and GLSL uniform block layout.
// 8 floats = 32 bytes — meets Metal/Vulkan 16-byte alignment requirement. // 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
struct PostFXUniforms { struct PostFXUniforms {
float vignette_strength; // 0 = none, ~0.8 = subtle float vignette_strength; // 0 = none, ~0.8 = subtle
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
float scanline_strength; // 0 = off, 1 = full float scanline_strength; // 0 = off, 1 = full
float screen_height; // logical height in pixels (for resolution-independent scanlines) float screen_height; // logical height in pixels (used by bleeding effect)
float mask_strength; // 0 = off, 1 = full phosphor dot mask float mask_strength; // 0 = off, 1 = full phosphor dot mask
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
float curvature; // 0 = flat, 1 = max barrel distortion float curvature; // 0 = flat, 1 = max barrel distortion
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding 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 pad1; // padding — keep struct at 48 bytes (3 × 16)
}; };
namespace Rendering { namespace Rendering {
@@ -56,6 +60,9 @@ namespace Rendering {
// Activa/desactiva escalado entero (integer scale) // Activa/desactiva escalado entero (integer scale)
void setScaleMode(bool integer_scale) override; void setScaleMode(bool integer_scale) override;
// Establece factor de supersampling (1 = off, 3 = 3×SS)
void setOversample(int factor) override;
private: private:
static auto createShaderMSL(SDL_GPUDevice* device, static auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source, const char* msl_source,
@@ -73,18 +80,24 @@ namespace Rendering {
Uint32 num_uniform_buffers) -> SDL_GPUShader*; Uint32 num_uniform_buffers) -> SDL_GPUShader*;
auto createPipeline() -> bool; auto createPipeline() -> bool;
auto reinitTexturesAndBuffer() -> bool; // Recrea textura y buffer con oversample actual
SDL_Window* window_ = nullptr; SDL_Window* window_ = nullptr;
SDL_GPUDevice* device_ = nullptr; SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; SDL_GPUGraphicsPipeline* pipeline_ = nullptr;
SDL_GPUTexture* scene_texture_ = nullptr; SDL_GPUTexture* scene_texture_ = nullptr;
SDL_GPUTransferBuffer* upload_buffer_ = nullptr; SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr; SDL_GPUSampler* sampler_ = nullptr; // NEAREST — para path sin supersampling
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR — para path con supersampling
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F}; PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
int tex_width_ = 0; int game_width_ = 0; // Dimensiones originales del canvas (sin SS)
int game_height_ = 0;
int tex_width_ = 0; // Dimensiones de la textura GPU (game × oversample_)
int tex_height_ = 0; int tex_height_ = 0;
int oversample_ = 1; // Factor SS actual (1 o 3)
float baked_scanline_strength_ = 0.0F; // Guardado para hornear en CPU
bool is_initialized_ = false; bool is_initialized_ = false;
bool vsync_ = true; bool vsync_ = true;
bool integer_scale_ = false; bool integer_scale_ = false;

View File

@@ -18,6 +18,7 @@ namespace Rendering {
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full) float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
float curvature = 0.0F; // Curvatura barrel CRT float curvature = 0.0F; // Curvatura barrel CRT
float bleeding = 0.0F; // Sangrado de color NTSC float bleeding = 0.0F; // Sangrado de color NTSC
bool supersampling{false}; // Supersampling 3×: scanlines horneadas en CPU + sampler LINEAR
}; };
/** /**
@@ -82,6 +83,13 @@ namespace Rendering {
*/ */
virtual void setScaleMode(bool /*integer_scale*/) {} virtual void setScaleMode(bool /*integer_scale*/) {}
/**
* @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 Verifica si el backend está usando aceleración por hardware * @brief Verifica si el backend está usando aceleración por hardware
* @return true si usa aceleración (OpenGL/Metal/Vulkan) * @return true si usa aceleración (OpenGL/Metal/Vulkan)

View File

@@ -718,6 +718,9 @@ namespace Options {
parseFloatField(p, "gamma", preset.gamma); parseFloatField(p, "gamma", preset.gamma);
parseFloatField(p, "curvature", preset.curvature); parseFloatField(p, "curvature", preset.curvature);
parseFloatField(p, "bleeding", preset.bleeding); parseFloatField(p, "bleeding", preset.bleeding);
if (p.contains("supersampling")) {
try { preset.supersampling = p["supersampling"].get_value<bool>(); } catch (...) {}
}
postfx_presets.push_back(preset); postfx_presets.push_back(preset);
} }
} }
@@ -759,6 +762,9 @@ namespace Options {
file << "# gamma: gamma correction input 2.4 / output 2.2\n"; file << "# gamma: gamma correction input 2.4 / output 2.2\n";
file << "# curvature: CRT barrel distortion\n"; file << "# curvature: CRT barrel distortion\n";
file << "# bleeding: NTSC horizontal colour bleeding\n"; file << "# bleeding: NTSC horizontal colour bleeding\n";
file << "# supersampling: 3x internal resolution, scanlines baked in CPU + linear filter\n";
file << "# true = consistent 33% scanlines at any zoom (slight softening at non-3x)\n";
file << "# false = sharp pixel art, scanlines depend on zoom (33% at 3x, 25% at 4x)\n";
file << "\n"; file << "\n";
file << "presets:\n"; file << "presets:\n";
file << " - name: \"CRT\"\n"; file << " - name: \"CRT\"\n";
@@ -769,6 +775,16 @@ namespace Options {
file << " gamma: 0.8\n"; file << " gamma: 0.8\n";
file << " curvature: 0.0\n"; file << " curvature: 0.0\n";
file << " bleeding: 0.0\n"; file << " bleeding: 0.0\n";
file << " supersampling: false\n";
file << " - name: \"CRT-SS\"\n";
file << " vignette: 0.6\n";
file << " scanlines: 0.7\n";
file << " chroma: 0.15\n";
file << " mask: 0.6\n";
file << " gamma: 0.8\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " supersampling: true\n";
file << " - name: \"NTSC\"\n"; file << " - name: \"NTSC\"\n";
file << " vignette: 0.4\n"; file << " vignette: 0.4\n";
file << " scanlines: 0.5\n"; file << " scanlines: 0.5\n";
@@ -777,6 +793,7 @@ namespace Options {
file << " gamma: 0.5\n"; file << " gamma: 0.5\n";
file << " curvature: 0.0\n"; file << " curvature: 0.0\n";
file << " bleeding: 0.6\n"; file << " bleeding: 0.6\n";
file << " supersampling: false\n";
file << " - name: \"CURVED\"\n"; file << " - name: \"CURVED\"\n";
file << " vignette: 0.5\n"; file << " vignette: 0.5\n";
file << " scanlines: 0.6\n"; file << " scanlines: 0.6\n";
@@ -785,6 +802,7 @@ namespace Options {
file << " gamma: 0.7\n"; file << " gamma: 0.7\n";
file << " curvature: 0.8\n"; file << " curvature: 0.8\n";
file << " bleeding: 0.0\n"; file << " bleeding: 0.0\n";
file << " supersampling: false\n";
file << " - name: \"SCANLINES\"\n"; file << " - name: \"SCANLINES\"\n";
file << " vignette: 0.0\n"; file << " vignette: 0.0\n";
file << " scanlines: 0.8\n"; file << " scanlines: 0.8\n";
@@ -793,6 +811,7 @@ namespace Options {
file << " gamma: 0.0\n"; file << " gamma: 0.0\n";
file << " curvature: 0.0\n"; file << " curvature: 0.0\n";
file << " bleeding: 0.0\n"; file << " bleeding: 0.0\n";
file << " supersampling: false\n";
file << " - name: \"SUBTLE\"\n"; file << " - name: \"SUBTLE\"\n";
file << " vignette: 0.3\n"; file << " vignette: 0.3\n";
file << " scanlines: 0.4\n"; file << " scanlines: 0.4\n";
@@ -801,6 +820,7 @@ namespace Options {
file << " gamma: 0.3\n"; file << " gamma: 0.3\n";
file << " curvature: 0.0\n"; file << " curvature: 0.0\n";
file << " bleeding: 0.0\n"; file << " bleeding: 0.0\n";
file << " supersampling: false\n";
file.close(); file.close();

View File

@@ -124,6 +124,7 @@ namespace Options {
float gamma{0.0F}; // Corrección gamma input 2.4 / output 2.2 (0.0 = off, 1.0 = plena) float gamma{0.0F}; // Corrección gamma input 2.4 / output 2.2 (0.0 = off, 1.0 = plena)
float curvature{0.0F}; // Distorsión barrel CRT (0.0 = plana, 1.0 = máxima curvatura) float curvature{0.0F}; // Distorsión barrel CRT (0.0 = plana, 1.0 = máxima curvatura)
float bleeding{0.0F}; // Sangrado de color NTSC horizontal Y/C (0.0 = off, 1.0 = máximo) float bleeding{0.0F}; // Sangrado de color NTSC horizontal Y/C (0.0 = off, 1.0 = máximo)
bool supersampling{false}; // 3x supersampling: scanlines horneadas en CPU + sampler LINEAR
}; };
// --- Variables globales --- // --- Variables globales ---