fix(vk): Sharpness samples one picture pixel apart, RCAS-style, the same at any window size #1256
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1256
Loading…
Reference in a new issue
No description provided.
Delete branch "sharpen-rcas"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Sharpness: neighbours one picture pixel apart, contrast-adaptive in the manner of AMD FidelityFX RCAS
The Insert menu's Sharpness runs in the window pass (
gpu/vk/shaders/fg_scale.comp, the only sharpen on the Vulkan path: direct presents and frame generation's made frames both go throughpresent_image->framegen_scale, same shader, same push constant). It took its four neighbours1.0 / vec2(d)apart, one window pixel. When the window is larger than the picture that is a fraction of a picture pixel (0.53 for the 2048 x 1152 picture on a 3840 x 2160 screen, 0.33 for 1280 x 720), so what a given setting did depended on the window's size rather than on the picture. This was found in #1071, which makes the same change in its copy of the shader; this PR is a separate implementation of it, on main, with measurements.What changes
1.0 / vec2(textureSize(src, 0))), or one window pixel when the window is the smaller of the two, so a downscale does not sharpen detail the window cannot show.RX_VK_UPSCALE_LOG's line says what the number means now (sharpness 40% (of RCAS's strongest, neighbours one source pixel apart)).fg_scale_comp_spv.hregenerated as the other headers are:glslangValidator -V --target-env vulkan1.2 --vn FG_SCALE_COMP_SPV(glslang 16.6.0, which reproduces main's header byte for byte before the change).Measured
Headless 3840 x 2160 output, the default 2048 x 1152 picture, Catmull-Rom, frame generation off, resolution 100%, Kokiri Forest, same save each run. Metric: mean |Laplacian| of luma, at window scale and at picture scale (the shot box-averaged back to 2048 x 1152). In-game shots, 3 per run; Sharpness 0 is the average of three runs (0.0339 / 0.0725), run-to-run noise about 3%:
Since animation moves things a little between runs, the same numbers were also taken on one frame: the game's 2048 x 1152 picture from a run, put through main's and this PR's shader math on the CPU, so every variant sees the same input.
At this window size main's sharpen did work: its neighbours went through the linear sampler, which blends the picture pixels around them. The gap grows with the window-to-picture ratio. The same frame at 1280 x 720 on 3840 x 2160 (3x): main +11 / +21 / +27% at 40 / 80 / 100%, this PR +20 / +59 / +87% (picture scale +7 / +13 / +15% against +10 / +20 / +24%). With Upscaling Off (the linear filter) at 2048 x 1152: main +16 / +30 / +36%, this PR +16 / +39 / +54%. Neither adds halos (both hold the result to the neighbourhood), and the RCAS lobe no longer pushes pixels next to black or white into clipping.
For players following the DLSS advice to set Sharpness to about 80%: main's 80% at 2048 x 1152 on a 4K screen is about this PR's 70 to 80% (70% by window-scale detail, 80% by picture-scale detail). About 75% is a good default for that advice after this change, and the value now carries over between window sizes.
Tests
tests/test_fg_scale_sharpen.py(inrun_all.py): the sharpen's neighbour spacing is the picture's size; the shader's ownsharpen()compiled as C++ against 200,000 random neighbourhoods at 0 / 10 / 40 / 80 / 100%: unchanged at 0, never outside the pixel's neighbourhood or 0..1, flat areas and black or white left as they are, and a blurred edge steeper as the setting rises. With glslangValidator present the shader compiles, and with the version the header names the header matches its output.python -I -B tests/run_all.py: 150 passed, 0 failed, 7 skipped (Linux). Linux build of vk + game.Credits
The finding, and the idea of an RCAS-style sharpen here, come from #1071. The sharpen follows the math of AMD FidelityFX RCAS (MIT license), written from its description, not copied.
Presets and existing settings
The Quality presets' Sharpness values were tuned for the old spacing. Each is re-tuned to give about the same sharpening as before on a 3840 x 2160 window at that preset's picture (2048 x 1152 docked, 1536 x 864 with the Handheld profile; Render resolution does not change the picture the window pass sees), on the slider's steps: Minimum 50 -> 30, Low 35 -> 30, Balanced 25 -> 30, High 30 -> 40, Ultra 40 -> 50. A Sharpness already saved in hookshot_options.ini is kept as it is: it is stronger than before when the window is much larger than the picture (most of all with the Handheld profile on a 4K screen), and the readme says so.
The window pass's SPIR-V header now starts with the sha256 of the fg_scale.comp it was made from, so the test checks it against the shader in CI as well (
python -I -B tests/test_fg_scale_sharpen.py --writemakes it).Self-review notes.
Checked and fine:
pc.sharp > 0.0still gates the whole block, andsharpen()at amount 0 returnscbit for bit anyway (lobe -0, divide by 1, clamp no-op).{int mode; float sharp;}, 8 bytes on both sides); the C++ change is only the log line.scale.croppedbefore the pass, sotextureSize(src, 0)is the cropped picture and 1/size is one picture pixel in uv for any ratio, per axis (non-integer and ultrawide fine). Downscale falls back to one window pixel. CLAMP_TO_EDGE linear sampler handles borders.(1-hi)/(4-4*mn4)white room, min over channels, lobe capped at 0.25-1/16,(c + lobe*sum)/(1+4*lobe); the 1/1024 floors keep it finite at black/white. Same 4 taps as before, a few extra ALU ops: no perf concern.Worth a look before merge:
apply_preset()in vk_menu.cpp or call out in the release notes that existing Sharpness values get stronger at low Resolution.// 1116.6.0), so CI (no matching glslang) never verifies fg_scale_comp_spv.h is the .comp compiled. Fine as-is since it reproduced main byte for byte locally, but the header is effectively unchecked in CI.#1166 (closed as duplicate of #1007) is about the game's own sharpening at native 4K, not this slider, so this PR neither fixes nor worsens it.
@ -39,2 +56,4 @@return clamp(sharp, lo, hi);}void main() {const ivec2 g = ivec2(gl_GlobalInvocationID.xy);RCAS divides by
4*mx4(neighbours only); this useshi, which includesc. That is never larger room than RCAS, so only more conservative, and the final clamp to [lo,hi] already guarantees 0..1. Fine, just not literally RCAS; maybe say so in the comment.@ -43,11 +62,13 @@ void main() {const vec2 uv = (vec2(g) + 0.5) / vec2(d);Taking the min room over r/g/b means any pixel with a channel near 0 or 1 (saturated colours, not just black/white) gets almost no lobe. RCAS does the same, so it's faithful, but it explains why the effect is weaker on strongly coloured areas than the luma metric suggests.
Checked:
srcis the cropped picture when a crop is requested (blit into scale.cropped first), so this is one picture pixel per axis at any ratio. Neighbours are bilinear at the output pixel's sub-texel phase whilecis Catmull-Rom/Lanczos, same as main; no new issue.@ -0,0 +1,182 @@"""The window pass's Sharpness (gpu/vk/shaders/fg_scale.comp): its neighbours are one picture (source) pixel apart, not oneWith a different glslang this only checks that the shader compiles, so CI never compares the committed header against the source. Acceptable, but it means the .h is trusted from the local regen.
488ec95. The window-pass picture is the output (2048 x 1152, or 1536 x 864 with the Handheld profile; Resolution % only changes the 3D inside it), so I matched each preset on a 3840 x 2160 window at its own picture: one game frame through main's and this PR's shader maths on the CPU, mean |Laplacian| of the window and of the picture (box-averaged back), the old value against this PR's on the slider's steps. Minimum 50 -> 30, Low 35 -> 30, Balanced 25 -> 30, High 30 -> 40, Ultra 40 -> 50 (the old handheld presets drop, the docked ones rise a step, since RCAS's lobe at low percentages is gentler than the old linear amount). On a 1920 x 1080 window the same values are a little softer than main's (0.6 to 0.7 of main's gain for the handheld presets, about 0.75 to 0.9 for the docked ones), which I preferred to over-sharpening 4K. Existing ini values are left as they are; the readme now says a Sharpness kept from an older version is stronger when the window is much larger than the picture.python -I -B tests/test_fg_scale_sharpen.py --writemakes the header (glslang's output plus that line). Inb48cb14.b48cb14.4a4a187), which carries the fixes for test_output_resolution and test_program_cache_cleanup.