diff --git a/src/platform/core/CMakeLists.txt b/src/platform/core/CMakeLists.txt index d167a48e7..7bc33534b 100644 --- a/src/platform/core/CMakeLists.txt +++ b/src/platform/core/CMakeLists.txt @@ -29,6 +29,8 @@ set(HEADERS src/device/shader/common.glsl.hpp src/device/shader/lcd_ghosting.glsl.hpp src/device/shader/output.glsl.hpp + src/device/shader/sharp_bilinear.glsl.hpp + src/device/shader/xbrz.glsl.hpp ) set(HEADERS_PUBLIC diff --git a/src/platform/core/include/platform/config.hpp b/src/platform/core/include/platform/config.hpp index a31f7e12e..eeffa6a91 100644 --- a/src/platform/core/include/platform/config.hpp +++ b/src/platform/core/include/platform/config.hpp @@ -28,6 +28,7 @@ struct PlatformConfig : Config { enum class Filter { Nearest, Linear, + Sharp, xBRZ } filter = Filter::Linear; diff --git a/src/platform/core/src/config.cpp b/src/platform/core/src/config.cpp index 19af9fc73..d13d4d363 100644 --- a/src/platform/core/src/config.cpp +++ b/src/platform/core/src/config.cpp @@ -80,6 +80,7 @@ void PlatformConfig::Load(std::string const& path) { const std::map filters{ { "nearest", Video::Filter::Nearest }, { "linear", Video::Filter::Linear }, + { "sharp", Video::Filter::Sharp }, { "xbrz", Video::Filter::xBRZ } }; @@ -179,6 +180,7 @@ void PlatformConfig::Save(std::string const& path) { switch(this->video.filter) { case Video::Filter::Nearest: filter = "nearest"; break; case Video::Filter::Linear: filter = "linear"; break; + case Video::Filter::Sharp: filter = "sharp"; break; case Video::Filter::xBRZ: filter = "xbrz"; break; } diff --git a/src/platform/core/src/device/ogl_video_device.cpp b/src/platform/core/src/device/ogl_video_device.cpp index 00d600f68..16cd6f674 100644 --- a/src/platform/core/src/device/ogl_video_device.cpp +++ b/src/platform/core/src/device/ogl_video_device.cpp @@ -17,6 +17,7 @@ #include "device/shader/color_agb.glsl.hpp" #include "device/shader/lcd_ghosting.glsl.hpp" #include "device/shader/output.glsl.hpp" +#include "device/shader/sharp_bilinear.glsl.hpp" #include "device/shader/xbrz.glsl.hpp" using Video = nba::PlatformConfig::Video; @@ -70,7 +71,8 @@ void OGLVideoDevice::Initialize() { } void OGLVideoDevice::ReloadConfig() { - if(config->video.filter == Video::Filter::Linear) { + if(config->video.filter == Video::Filter::Linear || + config->video.filter == Video::Filter::Sharp) { texture_filter = GL_LINEAR; } else { texture_filter = GL_NEAREST; @@ -184,6 +186,14 @@ void OGLVideoDevice::CreateShaderPrograms() { } break; } + // Sharp bilinear. + case Video::Filter::Sharp: { + auto [success, program] = CompileProgram(sharp_bilinear_vert, sharp_bilinear_frag); + if (success) { + shader_passes.push_back({program}); + } + break; + } // Plain linear/nearest-interpolated output. case Video::Filter::Nearest: case Video::Filter::Linear: { diff --git a/src/platform/core/src/device/shader/sharp_bilinear.glsl.hpp b/src/platform/core/src/device/shader/sharp_bilinear.glsl.hpp new file mode 100644 index 000000000..10fe971fb --- /dev/null +++ b/src/platform/core/src/device/shader/sharp_bilinear.glsl.hpp @@ -0,0 +1,67 @@ +/* + Author: rsn8887 (based on TheMaister) + License: Public domain + + This is an integer prescale filter that should be combined + with a bilinear hardware filtering (GL_BILINEAR filter or some such) to achieve + a smooth scaling result with minimum blur. This is good for pixelgraphics + that are scaled by non-integer factors. + + The prescale factor and texel coordinates are precalculated + in the vertex shader for speed. +*/ + +#pragma once + +constexpr auto sharp_bilinear_vert = R"( + #version 330 core + + layout(location = 0) in vec2 position; + layout(location = 1) in vec2 uv; + + out vec2 v_uv; + out vec2 precalc_texel; + out vec2 precalc_scale; + + uniform vec2 u_output_size; + + void main() { + gl_Position = vec4(position, 0.0, 1.0); + v_uv = vec2(uv.x, 1.0 - uv.y); + + const vec2 input_size = vec2(240, 160); + precalc_scale = max(floor(u_output_size / input_size), vec2(1.0, 1.0)); + precalc_texel = v_uv * input_size; + } +)"; + +constexpr auto sharp_bilinear_frag = R"( + #version 330 core + + in vec2 v_uv; + in vec2 precalc_texel; + in vec2 precalc_scale; + + layout(location = 0) out vec4 frag_color; + + uniform sampler2D u_input_map; + + void main() { + vec2 texel = precalc_texel; + vec2 scale = precalc_scale; + vec2 texel_floored = floor(texel); + vec2 s = fract(texel); + vec2 region_range = 0.5 - 0.5 / scale; + + // Figure out where in the texel to sample to get correct pre-scaled bilinear. + // Uses the hardware bilinear interpolator to avoid having to sample 4 times manually. + + vec2 center_dist = s - 0.5; + vec2 f = (center_dist - clamp(center_dist, -region_range, region_range)) * scale + 0.5; + + vec2 mod_texel = texel_floored + f; + + const vec2 input_size = vec2(240, 160); + frag_color = vec4(texture(u_input_map, mod_texel / input_size).rgb, 1.0); + } +)"; diff --git a/src/platform/qt/src/widget/main_window.cpp b/src/platform/qt/src/widget/main_window.cpp index 3f50ad24e..5207f67b3 100644 --- a/src/platform/qt/src/widget/main_window.cpp +++ b/src/platform/qt/src/widget/main_window.cpp @@ -162,6 +162,7 @@ void MainWindow::CreateVideoMenu(QMenu* parent) { CreateSelectionOption(menu->addMenu(tr("Filter")), { { "Nearest", nba::PlatformConfig::Video::Filter::Nearest }, { "Linear", nba::PlatformConfig::Video::Filter::Linear }, + { "Sharp", nba::PlatformConfig::Video::Filter::Sharp }, { "xBRZ", nba::PlatformConfig::Video::Filter::xBRZ } }, &config->video.filter, false, reload_config);