fix(vk): a picture larger than the window is filtered over all its pixels, not sampled #307
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!307
Loading…
Reference in a new issue
No description provided.
Delete branch "Amateurwizrd/Longshot:fix/downscale-filter"
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?
Why
An Output resolution larger than the window (3840x2160 or 5120x2880 in a 2560x1440 window, 5K on a 4K screen) is meant to be the sharp, smooth choice: drawn bigger, then scaled down. The window pass sampled that picture instead of averaging it, so fine detail (grass, foliage, fences, text edges) came out as shimmer and moiré, worse with Catmull-Rom or Lanczos than with the filter Off.
What changed
filtered()ingpu/vk/shaders/fg_scale.compstretches the filter by the picture's pixels per window pixel when the picture is the larger (up to 4x, 16 x 16 taps). Catmull-Rom and Lanczos read 4 x 4 of the picture whatever the shrink and skipped most of it; at 2x a window pixel now reads the 8 x 8 its kernel covers. Off becomes a stretched linear (tent) filter when shrinking. A window as large as the picture or larger is filtered exactly as before (the same 4 x 4 taps, the same weights; Off the same hardware bilinear).framegen_scale()ingpu/vk/vk_framegen.cpptakes the pass whenever the picture is larger than the window. With the filter Off and Sharpness 0 the picture tookblit_crop_to_rect'svkCmdBlitImage(VK_FILTER_LINEAR), which reads 2 x 2 texels whatever the shrink and aliases too.fg_scale_comp_spv.handfg_scale_pq_comp_spv.hregenerated (tests/test_fg_scale_sharpen.py --write, glslangValidator from Vulkan SDK 1.4.341.1;main's headers came from a newer glslang, so the test compares their hash line, not their bytes).tests/test_fg_scale_sharpen.pycompiles the shader's ownkernel()andreach()as C++ and resamples sines finer than the window at 1.33x, 1.5x, 2x and 2.67x shrinks: at most 0.3 of their swing may come through (0.02 to 0.27 now; the same resampling withmain's fixed 4 x 4 taps or a 2 x 2 bilinear lets 0.27 to 0.89 through, from a numpy model of the old code).Flags and settings
None. No switch: the old behaviour is a bug, and a picture no larger than the window is untouched.
Measurements
main:
514591a3(2026-11-13)this PR: fix/downscale-filter @
f057a4e4Hardware: NVIDIA GeForce RTX 5070 Ti, driver 621.01, AMD Ryzen 7 7800X3D, 32 GB, Windows 11
Scene: Kokiri Forest, standing at a fixed save; Output resolution 5120x2880 (
HOS_OUTPUT_RES) in a 2560x1440 window, Render resolution 75 %, the game's own upscaler, no frame generation, no frame capMethod: PresentMon, 60 s a leg after 15 s settle; 3 rounds, the two builds alternating in each round; the same game folder with only
longshot_vk.dllswapped (both built withRX_VK_ONLY=1, same toolchain). Medians of the three rounds.How to reproduce: Output resolution larger than the window (5120x2880 in a 2560x1440 window), stand still in Kokiri Forest, read GPU busy from PresentMon or the FPS counter's Breakdown.
514591a3f057a4e4GPU busy with Lanczos rose by 0.30, 0.34 and 0.27 ms in the three rounds: the cost of reading the 8 x 8 the 2x shrink needs. Off: 0.03 ms less in every round. Frame time is not GPU-bound here and did not move.
Testing
Tested:
python -I -B tests/run_all.pyon Windows 11 with llvm-mingw: 490 passed, 10 failed, 33 skipped. Seven of the failures fail the same way onmain514591a3(test_nup_paths,test_check_third_party,test_gpu_hang_watch,test_menu_i18n,test_portable_builder_startup,test_portable_builder_home,test_release). The other three were this PC's setup:test_fpx_stickyandtest_spawn_emitpass withLONGSHOT_MINGWset, andtest_setup_tar_diagnosticspassed when run again. In the game on the PC above: 5120x2880 in a 2560x1440 window with Lanczos/Sharpness 50 and Off/Sharpness 0, 12 measured legs and 4 screenshot legs, no device loss, the picture drawn whole in both filters.Not tested: Native HDR's 10-bit pass (
fg_scale_pq) with a picture larger than the window; the ultrawide menu sides while shrinking; frame generation on; AMD and Intel GPUs; Linux. A side-by-side picture of the shimmer: Navi and the particles move between launches, so still shots of the two builds don't line up; the game-free check measures the aliasing instead.Needs other testers:
main(T-Z9VV)Screenshots
Nothing to compare as a still: the change shows as less shimmer in motion. The game-free check's numbers are under What changed.
Risk and rollback
GPU cost when shrinking with Catmull-Rom or Lanczos: about 0.3 ms at 2x on an RTX 5070 Ti, up to 16 x 16 taps at the 4x cap (a 5K picture in a 1280x720 window). Shrinking more than 4x aliases again, as before. A picture no larger than the window is filtered as on
main. Reverting the commit restores the old filter.Reviewer notes
filtered()is the change; at stretch 1 its tap range is the oldb - 1 .. b + 2. A mip chain would make shrinks past 4x clean; left out as nobody plays a 5K picture in a quarter-size window.Issues
None.
Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.WIP: fix(vk): a picture larger than the window is filtered over all its pixels, not sampledto fix(vk): a picture larger than the window is filtered over all its pixels, not sampledCode review of
f057a4e4(read only, nothing built or run here).The change is sound.
b-1 .. b+2, and filter Off with no shrink still takes the hardware bilinear, so a picture no larger than the window is filtered as on main.filtered(), so the menu sides are unaffected.sis picture overdst, anduvspans all ofdst, so the stretch is right with ultrawide sides too.29ada605…, and that matchesfg_scale.compatf057a4e4(LF, as the test hashes it). Their bytes come from glslang 1116.2.0, older than main's 1116.6.0, so the test doesn't compare bytes against a different glslang. That's expected, noted only.What has to change before merge (checklist, CONTRIBUTING.md "delete what stopped applying", AGENTS.md):
docs/tested-configurations.mdrows. The 12 measured legs (3 rounds x 2 filters x main/PR, RTX 5070 Ti) need rows, and the If applicable box for them is missing from the checklist.vk_framegen.cppand the two*_spv.hare host code (pr statushints the same). Keep the box: tick it if it built on Linux, or leave it unticked with the reason.f057a4e4are still pending (run 2629). The CI checks and the Description check need to pass first.A suggestion (inline, not blocking): with Sharpness above 0, RCAS's neighbours are still single bilinear reads of the raw picture, so part of the aliasing comes back through the sharpen when shrinking.
v3.0.0: yes, once 1 and 2 are fixed (description and docs only, no code change).
@ -35,3 +45,3 @@vec3 filtered(vec2 uv) {if (pc.mode == 0) return textureLod(src, uv, 0.0).rgb;const vec2 size = vec2(textureSize(src, 0));const vec2 s = clamp(size / vec2(imageSize(dst)), vec2(1.0), vec2(kMaxStretch));Suggestion, not blocking. Shrinking,
cis now properly filtered, butmain()'s sharpen still reads its four neighbours with singletextureLodbilinear taps of the raw picture, one window pixel apart (e = max(1/size, 1/d)). At 2x those are 2 x 2 reads of a 4 x 4 footprint, so with Sharpness > 0 (every preset) the RCAS lobe adds back part of the aliasing this PR removes. The Lanczos/Sharpness 50 legs you measured are that case. Two options: take the neighbours throughfiltered()too (four more filter evaluations, so cost is a concern at 4x), or compare against a game-free check likeshrink_aliaswith the sharpen applied.@ -599,1 +598,4 @@if (!source.w || !source.h) return VK_NULL_HANDLE;// (a picture larger than the window takes the pass even with the filter Off: the blit's linear filter reads 2 x 2 of the// picture whatever the shrink, and fine detail shimmered)const bool shrink = source.w > dw || source.h > dh;This brings Native HDR (
pq) with the filter Off intofg_scale_pqwhenever the picture is larger than the window. Before, that path went toframegen_hdrwith the unscaled view, or the blit. That's a real change of route for HDR players in a window, and Testing lists it as untested. Not blocking, but worth one look in Native HDR before the release, or a line in Risk.A follow-up found while checking another change against this one: with every window effect off, an oversized picture still takes the old path, so the case this pull request names ("With the filter Off and Sharpness 0 the picture took
blit_crop_to_rect'svkCmdBlitImage(VK_FILTER_LINEAR)") is not reached by the new downscale in a plain window.Where: on main
cf605c85,gpu/vk/vk_replay.cppline 3969 asks for the sampled view only whenopts.upscale || opts.sharpness || dev.hdr || wide_sides_possible(). Shrinking alone is not in that condition. Without a view, line 4616 does not callframegen_scale, and the frame goes toblit_crop_to_rectat line 4630 as before.Checked in a real window on Windows 11 with Intel Arc (driver 32.0.101.9175), native build of main
cf605c85, SDR, frame generation off, sharpness 0, the still title menu, withRX_VK_PRESENT_LOG=1. The only difference between the two runs of each pair is the Window scaling filter (Off against Catmull-Rom):route=direct view=0route=scaled view=1route=direct view=0route=scaled view=1At exactly 2:1 the two pictures look almost alike in 1:1 crops, which fits: a 2 x 2 linear read covers every source pixel at that ratio. The pictures in this pull request are also 2:1 (5120x2880 into 2560x1440), so they could not show it. At 3:1 the all-off small text is thinner and harsher and the logo lettering coarser than through the scaled route:
What this does and does not show: the route lines settle that the all-off case skips the new pass. The crops compare Off against Catmull-Rom, not Off through the scaled route, so they show the direct path's coarser sampling at 3:1 rather than how the new tent filter would look there. Still pictures only; nothing is claimed about shimmer in motion. Adding "the picture is larger than the destination" to the condition at line 3969 would send this case through
framegen_scaleas the description intends. Not checked: other GPUs, Linux in a real window, HDR, frame generation on.