From c87779cc09364d04890614a4e5f39557c8322549 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 22 Mar 2026 21:24:20 +0100 Subject: [PATCH] imlementant supersampling --- data/shaders/postfx.frag | 18 +- source/core/rendering/screen.cpp | 5 +- .../core/rendering/sdl3gpu/sdl3gpu_shader.cpp | 206 +++++++++++++++--- .../core/rendering/sdl3gpu/sdl3gpu_shader.hpp | 19 +- source/core/rendering/shader_backend.hpp | 22 +- source/game/options.cpp | 20 ++ source/game/options.hpp | 17 +- 7 files changed, 245 insertions(+), 62 deletions(-) diff --git a/data/shaders/postfx.frag b/data/shaders/postfx.frag index d88e21e..9d13c36 100644 --- a/data/shaders/postfx.frag +++ b/data/shaders/postfx.frag @@ -24,8 +24,8 @@ 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 (for future animated effects) - float pad0; // padding — 48 bytes total (3 × 16) - float pad1; + float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS) + float pad1; // padding — 48 bytes total (3 × 16) } u; // YCbCr helpers for NTSC bleeding @@ -68,15 +68,17 @@ void main() { // Muestra base 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; 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_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0/tw, 0.0)).rgb); - vec3 ycc_l1 = 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/tw, 0.0)).rgb); - vec3 ycc_r2 = 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*step, 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*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; colour = mix(base, ycc_to_rgb(ycc), u.bleeding); } else { diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index c7462c3..830d9c2 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -463,7 +463,10 @@ auto loadData(const std::string& filepath) -> std::vector { void Screen::applyCurrentPostFXPreset() { if (shader_backend_ && !Options::postfx_presets.empty()) { const auto& p = Options::postfx_presets[static_cast(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); } } diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp index 0556c17..492dd0a 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp @@ -56,7 +56,7 @@ struct PostFXUniforms { float bleeding; float pixel_scale; float time; - float pad0; + float oversample; // 1.0 = sin SS, 3.0 = 3× supersampling float pad1; }; @@ -102,15 +102,17 @@ fragment float4 postfx_fs(PostVOut in [[stage_in]], // Muestra base 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; if (u.bleeding > 0.0f) { - float tw = float(scene.get_width()); - 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_l1 = 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/tw, 0.0f)).rgb); - float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f/tw, 0.0f)).rgb); + 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 { @@ -199,9 +201,12 @@ namespace Rendering { float fw = 0.0F; float fh = 0.0F; SDL_GetTextureSize(texture, &fw, &fh); - tex_width_ = static_cast(fw); - tex_height_ = static_cast(fh); - uniforms_.screen_height = fh; // Altura lógica del juego (no el swapchain físico) + game_width_ = static_cast(fw); + game_height_ = static_cast(fh); + tex_width_ = game_width_ * oversample_; + tex_height_ = game_height_ * oversample_; + uniforms_.screen_height = static_cast(tex_height_); // Altura de la textura GPU + uniforms_.oversample = static_cast(oversample_); // ---------------------------------------------------------------- // 1. Create GPU device (solo si no existe ya) @@ -264,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 = {}; samp_info.min_filter = SDL_GPU_FILTER_NEAREST; @@ -280,6 +285,20 @@ namespace Rendering { 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 // ---------------------------------------------------------------- @@ -345,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) { if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; } @@ -355,7 +376,45 @@ namespace Rendering { SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError()); return; } - std::memcpy(mapped, pixels, static_cast(width * height * 4)); + + if (oversample_ <= 1) { + // Path sin supersampling: copia directa + std::memcpy(mapped, pixels, static_cast(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(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((SRC >> 16) & 0xFFU); + const auto FG = static_cast((SRC >> 8) & 0xFFU); + const auto FB = static_cast( SRC & 0xFFU); + + auto make_px = [ALPHA](float rv, float gv, float bv) -> Uint32 { + auto cl = [](float v) -> Uint32 { return static_cast(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_); } @@ -416,36 +475,45 @@ namespace Rendering { if (pass != nullptr) { 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 vy = 0.0F; float vw = 0.0F; float vh = 0.0F; if (integer_scale_) { - const int SCALE = std::max(1, std::min(static_cast(sw) / tex_width_, static_cast(sh) / tex_height_)); - vw = static_cast(tex_width_ * SCALE); - vh = static_cast(tex_height_ * SCALE); + const int SCALE = std::max(1, std::min(static_cast(sw) / game_width_, static_cast(sh) / game_height_)); + vw = static_cast(game_width_ * SCALE); + vh = static_cast(game_height_ * SCALE); } else { const float SCALE = std::min( - static_cast(sw) / static_cast(tex_width_), - static_cast(sh) / static_cast(tex_height_)); - vw = static_cast(tex_width_) * SCALE; - vh = static_cast(tex_height_) * SCALE; + static_cast(sw) / static_cast(game_width_), + static_cast(sh) / static_cast(game_height_)); + vw = static_cast(game_width_) * SCALE; + vh = static_cast(game_height_) * SCALE; } vx = std::floor((static_cast(sw) - vw) * 0.5F); vy = std::floor((static_cast(sh) - vh) * 0.5F); SDL_GPUViewport vp = {vx, vy, vw, vh, 0.0F, 1.0F}; SDL_SetGPUViewport(pass, &vp); - // Uniforms dinámicos: pixel_scale y time - uniforms_.pixel_scale = (tex_height_ > 0) - ? (vh / static_cast(tex_height_)) + // 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(game_height_)) : 1.0F; - uniforms_.time = static_cast(SDL_GetTicks()) / 1000.0F; + uniforms_.time = static_cast(SDL_GetTicks()) / 1000.0F; + uniforms_.oversample = static_cast(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 = {}; binding.texture = scene_texture_; - binding.sampler = sampler_; + binding.sampler = active_sampler; SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1); SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms)); @@ -482,6 +550,10 @@ namespace Rendering { SDL_ReleaseGPUSampler(device_, sampler_); sampler_ = nullptr; } + if (linear_sampler_ != nullptr) { + SDL_ReleaseGPUSampler(device_, linear_sampler_); + linear_sampler_ = nullptr; + } // device_ y el claim de la ventana se mantienen vivos } } @@ -550,12 +622,16 @@ namespace Rendering { void SDL3GPUShader::setPostFXParams(const PostFXParams& p) { uniforms_.vignette_strength = p.vignette; - uniforms_.scanline_strength = p.scanlines; - uniforms_.chroma_strength = p.chroma; - uniforms_.mask_strength = p.mask; - uniforms_.gamma_strength = p.gamma; - uniforms_.curvature = p.curvature; - uniforms_.bleeding = p.bleeding; + uniforms_.chroma_strength = p.chroma; + uniforms_.mask_strength = p.mask; + uniforms_.gamma_strength = p.gamma; + uniforms_.curvature = p.curvature; + 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) { @@ -569,4 +645,68 @@ namespace Rendering { 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(tex_height_); + uniforms_.oversample = static_cast(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(tex_width_); + tex_info.height = static_cast(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(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 diff --git a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp index 1a900b3..df9d704 100644 --- a/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp +++ b/source/core/rendering/sdl3gpu/sdl3gpu_shader.hpp @@ -19,8 +19,8 @@ struct PostFXUniforms { 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 pad0; // padding — keep struct at 48 bytes (3 × 16) - float pad1; + float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS) + float pad1; // padding — keep struct at 48 bytes (3 × 16) }; namespace Rendering { @@ -60,6 +60,9 @@ namespace Rendering { // Activa/desactiva escalado entero (integer scale) void setScaleMode(bool integer_scale) override; + // Establece factor de supersampling (1 = off, 3 = 3×SS) + void setOversample(int factor) override; + private: static auto createShaderMSL(SDL_GPUDevice* device, const char* msl_source, @@ -77,18 +80,24 @@ namespace Rendering { Uint32 num_uniform_buffers) -> SDL_GPUShader*; auto createPipeline() -> bool; + auto reinitTexturesAndBuffer() -> bool; // Recrea textura y buffer con oversample actual SDL_Window* window_ = nullptr; SDL_GPUDevice* device_ = nullptr; SDL_GPUGraphicsPipeline* pipeline_ = nullptr; SDL_GPUTexture* scene_texture_ = 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, .pixel_scale = 1.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 oversample_ = 1; // Factor SS actual (1 o 3) + float baked_scanline_strength_ = 0.0F; // Guardado para hornear en CPU bool is_initialized_ = false; bool vsync_ = true; bool integer_scale_ = false; diff --git a/source/core/rendering/shader_backend.hpp b/source/core/rendering/shader_backend.hpp index cd3e1e4..f21b6b4 100644 --- a/source/core/rendering/shader_backend.hpp +++ b/source/core/rendering/shader_backend.hpp @@ -11,13 +11,14 @@ namespace Rendering { * Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp */ 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 - 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 vignette = 0.0F; // Intensidad de la viñeta + float scanlines = 0.0F; // Intensidad de las scanlines + float chroma = 0.0F; // Aberración cromática + 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 + bool supersampling{false}; // Supersampling 3×: scanlines horneadas en CPU + sampler LINEAR }; /** @@ -82,6 +83,13 @@ namespace Rendering { */ 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 * @return true si usa aceleración (OpenGL/Metal/Vulkan) diff --git a/source/game/options.cpp b/source/game/options.cpp index 1401faf..f3100a8 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -718,6 +718,9 @@ namespace Options { parseFloatField(p, "gamma", preset.gamma); parseFloatField(p, "curvature", preset.curvature); parseFloatField(p, "bleeding", preset.bleeding); + if (p.contains("supersampling")) { + try { preset.supersampling = p["supersampling"].get_value(); } catch (...) {} + } postfx_presets.push_back(preset); } } @@ -759,6 +762,9 @@ namespace Options { file << "# gamma: gamma correction input 2.4 / output 2.2\n"; file << "# curvature: CRT barrel distortion\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 << "presets:\n"; file << " - name: \"CRT\"\n"; @@ -769,6 +775,16 @@ namespace Options { file << " gamma: 0.8\n"; file << " curvature: 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 << " vignette: 0.4\n"; file << " scanlines: 0.5\n"; @@ -777,6 +793,7 @@ namespace Options { file << " gamma: 0.5\n"; file << " curvature: 0.0\n"; file << " bleeding: 0.6\n"; + file << " supersampling: false\n"; file << " - name: \"CURVED\"\n"; file << " vignette: 0.5\n"; file << " scanlines: 0.6\n"; @@ -785,6 +802,7 @@ namespace Options { file << " gamma: 0.7\n"; file << " curvature: 0.8\n"; file << " bleeding: 0.0\n"; + file << " supersampling: false\n"; file << " - name: \"SCANLINES\"\n"; file << " vignette: 0.0\n"; file << " scanlines: 0.8\n"; @@ -793,6 +811,7 @@ namespace Options { file << " gamma: 0.0\n"; file << " curvature: 0.0\n"; file << " bleeding: 0.0\n"; + file << " supersampling: false\n"; file << " - name: \"SUBTLE\"\n"; file << " vignette: 0.3\n"; file << " scanlines: 0.4\n"; @@ -801,6 +820,7 @@ namespace Options { file << " gamma: 0.3\n"; file << " curvature: 0.0\n"; file << " bleeding: 0.0\n"; + file << " supersampling: false\n"; file.close(); diff --git a/source/game/options.hpp b/source/game/options.hpp index 87815ff..1c29143 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -116,14 +116,15 @@ namespace Options { // Estructura para un preset de PostFX struct PostFXPreset { - std::string name; // Nombre del preset - float vignette{0.6F}; // Intensidad de la viñeta (0.0 = ninguna, 1.0 = máxima) - float scanlines{0.7F}; // Intensidad de las scanlines (0.0 = desactivadas, 1.0 = máximas) - float chroma{0.15F}; // Intensidad de la aberración cromática (0.0 = ninguna, 1.0 = máxima) - float mask{0.0F}; // Intensidad de la máscara de fósforo RGB (0.0 = desactivada, 1.0 = máxima) - 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 bleeding{0.0F}; // Sangrado de color NTSC horizontal Y/C (0.0 = off, 1.0 = máximo) + std::string name; // Nombre del preset + float vignette{0.6F}; // Intensidad de la viñeta (0.0 = ninguna, 1.0 = máxima) + float scanlines{0.7F}; // Intensidad de las scanlines (0.0 = desactivadas, 1.0 = máximas) + float chroma{0.15F}; // Intensidad de la aberración cromática (0.0 = ninguna, 1.0 = máxima) + float mask{0.0F}; // Intensidad de la máscara de fósforo RGB (0.0 = desactivada, 1.0 = máxima) + 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 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 ---