rendervulkan: Add PIXEL filter (band-limited pixel filter)

Adds a "pixel" filter which is a band-limited pixel filter based on
https://github.com/Themaister/Granite/blob/master/assets/shaders/inc/bandlimited_pixel_filter.h
by Hans-Kristian.

Closes: #712
This commit is contained in:
Joshua Ashton 2023-10-06 22:35:00 +01:00 committed by Joshie
parent e17627beef
commit 766f9b459c
8 changed files with 79 additions and 17 deletions

View file

@ -149,7 +149,7 @@ const char usage[] =
" -r, --nested-refresh game refresh rate (frames per second)\n"
" -m, --max-scale maximum scale factor\n"
" -S, --scaler upscaler type (auto, integer, fit, fill, stretch)\n"
" -F, --filter upscaler filter (linear, nearest, fsr, nis)\n"
" -F, --filter upscaler filter (linear, nearest, fsr, nis, pixel)\n"
" fsr => AMD FidelityFX™ Super Resolution 1.0\n"
" nis => NVIDIA Image Scaling v1.0.3\n"
" --sharpness, --fsr-sharpness upscaler sharpness from 0 (max) to 20 (min)\n"
@ -391,6 +391,8 @@ static enum GamescopeUpscaleFilter parse_upscaler_filter(const char *str)
return GamescopeUpscaleFilter::FSR;
} else if (strcmp(str, "nis") == 0) {
return GamescopeUpscaleFilter::NIS;
} else if (strcmp(str, "pixel") == 0) {
return GamescopeUpscaleFilter::PIXEL;
} else {
fprintf( stderr, "gamescope: invalid value for --filter\n" );
exit(1);

View file

@ -29,6 +29,7 @@ enum class GamescopeUpscaleFilter : uint32_t
NEAREST,
FSR,
NIS,
PIXEL,
FROM_VIEW = 255, // internal
};

View file

@ -3319,7 +3319,10 @@ void bind_all_layers(CVulkanCmdBuffer* cmdBuffer, const struct FrameInfo_t *fram
{
const FrameInfo_t::Layer_t *layer = &frameInfo->layers[i];
bool nearest = layer->isScreenSize() || layer->filter == GamescopeUpscaleFilter::NEAREST || (layer->filter == GamescopeUpscaleFilter::LINEAR && !layer->viewConvertsToLinearAutomatically());
bool nearest = layer->isScreenSize()
|| layer->filter == GamescopeUpscaleFilter::NEAREST
|| (layer->filter == GamescopeUpscaleFilter::LINEAR && !layer->viewConvertsToLinearAutomatically());
cmdBuffer->bindTexture(i, layer->tex);
cmdBuffer->setTextureSrgb(i, false);
cmdBuffer->setSamplerNearest(i, nearest);

View file

@ -246,7 +246,12 @@ struct vec2_t
static inline bool float_is_integer(float x)
{
return fabsf(ceilf(x) - x) <= 0.0001f;
return fabsf(ceilf(x) - x) <= 0.001f;
}
inline bool close_enough(float a, float b, float epsilon = 0.001f)
{
return fabsf(a - b) <= epsilon;
}
struct FrameInfo_t
@ -290,8 +295,8 @@ struct FrameInfo_t
}
bool isScreenSize() const {
return scale.x >= 0.99f && scale.x <= 1.01f &&
scale.y >= 0.99f && scale.y <= 1.01f &&
return close_enough(scale.x, 1.0f) &&
close_enough(scale.y, 1.0f) &&
float_is_integer(offset.x) &&
float_is_integer(offset.y);
}

View file

@ -266,7 +266,7 @@ void inputSDLThreadRun( void )
SDL_SetWindowFullscreen( g_SDLWindow, g_bFullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0 );
break;
case KEY_N:
g_wantedUpscaleFilter = GamescopeUpscaleFilter::NEAREST;
g_wantedUpscaleFilter = GamescopeUpscaleFilter::PIXEL;
break;
case KEY_B:
g_wantedUpscaleFilter = GamescopeUpscaleFilter::LINEAR;

View file

@ -1,5 +1,45 @@
#include "colorimetry.h"
vec4 sampleRegular(sampler2D tex, vec2 coord, uint colorspace) {
vec4 color = textureLod(tex, coord, 0);
color.rgb = colorspace_plane_degamma_tf(color.rgb, colorspace);
return color;
}
// To be considered pseudo-bandlimited, upscaling factor must be at least 2x.
const float bandlimited_PI = 3.14159265359;
const float bandlimited_PI_half = 0.5 * bandlimited_PI;
// size: resolution of sampled texture
// inv_size: inverse resolution of sampled texture
// extent: Screen-space gradient of UV in texels. Typically computed as (texture resolution) / (viewport resolution).
// If screen is rotated by 90 or 270 degrees, the derivatives need to be computed appropriately.
// For uniform scaling, none of this matters.
// extent can be multiplied to achieve LOD bias.
// extent must be at least 1.0 / 256.0.
vec4 sampleBandLimited(sampler2D samp, vec2 uv, vec2 size, vec2 inv_size, vec2 extent, uint colorspace, bool unnormalized)
{
// Josh:
// Clamp to behaviour like 4x scale (0.25).
//
// Was defaulted to 2x before (0.5), which is 1px, but gives blurry result
// on Cave Story (480p) -> 800p on Deck.
// TODO: Maybe make this configurable?
const float max_extent = 0.25f;
// Get base pixel and phase, range [0, 1).
vec2 pixel = uv * (unnormalized ? vec2(1.0f) : size) - 0.5;
vec2 base_pixel = floor(pixel);
vec2 phase = pixel - base_pixel;
// We can resolve the filter by just sampling a single 2x2 block.
// Lerp between normal sampling at LOD 0, and bandlimited pixel filter at LOD -1.
vec2 shift = 0.5 + 0.5 * sin(bandlimited_PI_half * clamp((phase - 0.5) / min(extent, vec2(max_extent)), -1.0, 1.0));
uv = (base_pixel + 0.5 + shift) * (unnormalized ? vec2(1.0f) : inv_size);
return sampleRegular(samp, uv, colorspace);
}
uint pseudo_random(uint seed) {
seed ^= (seed << 13);
seed ^= (seed >> 17);
@ -101,12 +141,6 @@ vec4 sampleBilinear(sampler2D tex, vec2 coord, uint colorspace, bool unnormalize
return mix(temp1, temp0, filterWeight.y);
}
vec4 sampleRegular(sampler2D tex, vec2 coord, uint colorspace, bool unnormalized) {
vec4 color = textureLod(tex, coord, 0);
color.rgb = colorspace_plane_degamma_tf(color.rgb, colorspace);
return color;
}
vec4 sampleLayerEx(sampler2D layerSampler, uint offsetLayerIdx, uint colorspaceLayerIdx, vec2 uv, bool unnormalized) {
vec2 coord = ((uv + u_offset[offsetLayerIdx]) * u_scale[offsetLayerIdx]);
vec2 texSize = textureSize(layerSampler, 0);
@ -126,10 +160,17 @@ vec4 sampleLayerEx(sampler2D layerSampler, uint offsetLayerIdx, uint colorspaceL
uint colorspace = get_layer_colorspace(colorspaceLayerIdx);
vec4 color;
if (u_shaderFilter[offsetLayerIdx] == filter_linear_emulated)
if (u_shaderFilter[offsetLayerIdx] == filter_pixel) {
vec2 output_res = texSize / u_scale[offsetLayerIdx];
vec2 extent = max((texSize / output_res), vec2(1.0 / 256.0));
color = sampleBandLimited(layerSampler, coord, unnormalized ? vec2(1.0f) : texSize, unnormalized ? vec2(1.0f) : vec2(1.0f) / texSize, extent, colorspace, unnormalized);
}
else if (u_shaderFilter[offsetLayerIdx] == filter_linear_emulated) {
color = sampleBilinear(layerSampler, coord, colorspace, unnormalized);
else
color = sampleRegular(layerSampler, coord, colorspace, unnormalized);
}
else {
color = sampleRegular(layerSampler, coord, colorspace);
}
color.rgb = apply_layer_color_mgmt(color.rgb, colorspace);
return color;

View file

@ -20,6 +20,7 @@ const int filter_linear_emulated = 0;
const int filter_nearest = 1;
const int filter_fsr = 2;
const int filter_nis = 3;
const int filter_pixel = 4;
const int filter_from_view = 255;
const int EOTF_Gamma22 = 0;

View file

@ -2198,6 +2198,13 @@ paint_window(steamcompmgr_win_t *w, steamcompmgr_win_t *scaleW, struct FrameInfo
layer->filter = (w->isOverlay || w->isExternalOverlay) ? GamescopeUpscaleFilter::LINEAR : g_upscaleFilter;
layer->colorspace = lastCommit->colorspace();
if (layer->filter == GamescopeUpscaleFilter::PIXEL)
{
// Don't bother doing more expensive filtering if we are sharp + integer.
if (float_is_integer(currentScaleRatio_x) && float_is_integer(currentScaleRatio_y))
layer->filter = GamescopeUpscaleFilter::NEAREST;
}
if ( flags & PaintWindowFlag::BasePlane )
{
BaseLayerInfo_t basePlane = {};
@ -2485,7 +2492,9 @@ paint_all(bool async)
if ( !BIsNested() && g_nOutputRefresh != nTargetRefresh && g_uDynamicRefreshEqualityTime + g_uDynamicRefreshDelay < now )
drm_set_refresh( &g_DRM, nTargetRefresh );
bool bNeedsNearest = g_upscaleFilter == GamescopeUpscaleFilter::NEAREST && frameInfo.layers[0].scale.x != 1.0f && frameInfo.layers[0].scale.y != 1.0f;
bool bLayer0ScreenSize = close_enough(frameInfo.layers[0].scale.x, 1.0f) && close_enough(frameInfo.layers[0].scale.y, 1.0f);
bool bNeedsCompositeFromFilter = (g_upscaleFilter == GamescopeUpscaleFilter::NEAREST || g_upscaleFilter == GamescopeUpscaleFilter::PIXEL) && !bLayer0ScreenSize;
// Disable partial composition for now until we get
// composite priorities working in libliftoff + also
@ -2501,7 +2510,7 @@ paint_all(bool async)
bNeedsFullComposite |= frameInfo.useFSRLayer0;
bNeedsFullComposite |= frameInfo.useNISLayer0;
bNeedsFullComposite |= frameInfo.blurLayer0;
bNeedsFullComposite |= bNeedsNearest;
bNeedsFullComposite |= bNeedsCompositeFromFilter;
bNeedsFullComposite |= bDrewCursor;
bNeedsFullComposite |= g_bColorSliderInUse;
bNeedsFullComposite |= fadingOut;