Skip to content

feat(vk): AMD FSR 3.1 as the default Upscaler without matrix hardware - #2110

Closed
addisonbair wants to merge 1 commit into
NotRexed:mainfrom
addisonbair:fsr-default
Closed

addisonbair wants to merge 1 commit into
NotRexed:mainfrom
addisonbair:fsr-default

Conversation

@addisonbair

Copy link
Copy Markdown
Contributor

On GPUs without matrix hardware (AMD RDNA2 such as the Steam Deck, Intel), the game's own upscaler runs its network's matrix multiplies in a shader function. On the Steam Deck that single dispatch costs ~12 ms of the frame (#1000). Switching the Upscaler to AMD FSR 3.1 removes it: 11.9 → 13.9 fps docked in the #1000 measurements, and the network dispatch drops out of the GPU profile. Today a Deck user has to find that menu row themselves.

What changes

  • When the matrix multiplies run in software (coopmat_software(), i.e. mma_software) and FSR 3.1 is supported, an Upscaler nobody has chosen defaults to AMD FSR 3.1. A line in the log says so.
  • "Nobody has chosen" means no hookshot_options.ini yet, or a file from before options_version=5. Older files wrote dlss= whether or not it was chosen, which is the same reasoning as the options_version 4 step for Descriptor reuse. Saving now writes options_version=5, so a choice made in the menu after this is kept.
  • Where the cooperative matrices run on hardware (NVIDIA RTX, for one), nothing changes and the game's own upscaler stays.
  • The decision runs at the end of create_device, after the push-descriptor limit is read, because fsr3::supported() needs that limit and caches its answer.

Testing

  • tests/run_all.py: the same results on clean main (1a43a1e) and with the patch. Every failure is the same on both and comes from tools missing in my container (glslang etc.); none are new.
  • Headless start-up on a ThinkPad P16 (Intel Arc ARL iGPU, NVIDIA RTX PRO 2000):
    • Intel, no options file → FSR 3.1 by default
    • NVIDIA, no options file → the game's own upscaler, unchanged
    • Intel, options_version=5, dlss=0 → kept (chosen)
    • Intel, options_version=4, dlss=0 → FSR 3.1
    • Intel, options_version=4, dlss=1 → kept (not the game's own)

🤖 Generated with Claude Code

@hckrman101

Copy link
Copy Markdown
Collaborator

Heads-up: this conflicts with main now. Could you rebase onto main and resolve the conflicts? Thanks!

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic holds up, and the reasoning matches the options_version 4 step. I checked the ordering:

  • menu_init() runs from pick_device() before create_device(), so upscaler_unchosen is already set when menu_upscaler_default() runs at the end of create_device() (gpu/vk/vk_main.cpp:501).
  • By then coopmat_init() has run and dev.max_push_descriptors is read, so fsr3::supported() gives an answer it can cache.
  • A chosen DLSS or FSR 3.1 is left alone. Hardware matrices change nothing.
  • fsr3_wanted() still gives way to an upscaler chosen through the environment (temporal_env_chosen()).

The menu tests that read vk_menu.cpp and vk_main.cpp pass at this head (51 checks).

What it still needs:

  • blocking Merge conflict with main in gpu/vk/vk_menu.cpp and gpu/vk/vk_menu.h, as @hckrman101 noted. Main has moved a lot there: FSR 4 (#2342), DLSS frame generation and others. When you rebase, check that upscaler_rows::GAME/FSR3 and the row indices still mean the same thing. Also check that no other PR has taken options_version=5 in the meantime. CI hasn't run on this PR yet (only the title check), so it will get its first full run after the rebase.
  • should fix It needs an in-game check on a Steam Deck. The Deck is Tier 1 in docs/workflow.md and the main hardware this targets. So far the testing is headless start-up on Intel Arc and NVIDIA, plus fps numbers taken from #1000. This changes what every Deck and Intel player sees on first start, so please add a run on a Deck (or an RDNA2 card) showing the picture with FSR 3.1 by default. CONTRIBUTING asks for screenshots when a change shows on screen. A before/after at the same spot, plus the log line, would cover it.
  • question Players with an older options file who picked "the game's own" on purpose will be moved to FSR 3.1 once. That's the same trade as the Descriptor reuse step, and probably the right call. It's worth a line in the release notes so nobody thinks their setting was lost.
  • minor gpu/vk/vk_menu.cpp:39 adds a second #include "vk_fsr3.h"; it's already included on line 41. The comment above options_version (line 1260) lists what 2, 3 and 4 mean, so add 5 ("the Upscaler: settled once the device is known, see menu_upscaler_default").

hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
…rt, F1 or Esc") (#2197)

On PC, Esc is the key that opens a game's menu, and players expect it:
after #1998 added Esc as an opt-in Menu shortcut
choice, null asked in #dev-work why Esc closes the Insert menu but
doesn't open it. This makes that choice the default,
for existing players too.

**What changed**
- `gpu/vk/vk_menu.h`: `menu_shortcut` defaults to 2, **Insert, F1 or
Esc**. On a closed menu Esc opens it; while it is
open Esc still goes back a page or closes it. F1 always works, and the
row still offers Insert or F1 and F1 only.
- **Existing options move too (`options_version` 5).** null is an
existing player, and so is nearly everyone who would
notice this. The menu writes every row on save, so the old default,
`menu_shortcut=0` (Insert or F1), is in every
options file whether the player chose it or not. A default that only new
installs get wouldn't reach them. Like Game
speed (version 2) and Descriptor reuse (version 4), `menu_init` now
moves an older file's 0 to 2. Only 0 moves:
- Insert and F1 still open the menu after the move, so nothing is taken
away; Esc is added.
- F1 only (1) can only have been chosen on purpose, so it stays. 2
stays.
- `save()` writes `options_version=5`, so a player who picks Insert or
F1 from now on keeps it.
- #2110 (FSR 3.1 as the default Upscaler without matrix hardware) also
takes version 5 for its own move. Whichever
lands second takes 6; it's a one-line renumber plus its test, and I'll
do it here if this one is second.
- Play is unchanged. Esc still lets a captured mouse go first, and the
menu it opens gives the capture back when it
closes (`esc_had_capture`, #1998 and #2035, unchanged here). No game
control can be bound to Esc (`bindable_keys`,
`host/nn_hid.c` defaults), and a gamepad's back button counts as Esc
only while the menu is open.
- `readme.md`: the menu keys paragraph (with the move of older options),
and the Menu shortcut row's values and default
  in the options table.
- `tests/test_menu_keys.py`:
- It compiles `menu_tick`'s Esc block with the real `Options` from
`vk_menu.h` and the row's names
(`tx_menu_shortcut`). Out of the box the shortcut is "Insert, F1 or
Esc", and Esc opens a closed menu without the same
press going back a page. With Insert or F1 or F1 only, Esc leaves it
closed. Both polled and queued presses.
- It also compiles `menu_init`'s reading of `hookshot_options.ini` and
loads real files through it. From before
version 5 (versions 2 to 4, or no `options_version` line): 0 loads as 2,
1 and 2 stay, no line gets 2. At the
version `save()` writes now, read from `save()` itself: 0, 1 and 2 all
stay.
- This replaces the text check that pinned the old default (`int
menu_shortcut = 0;`).

**Testing**
- `tests/test_menu_keys.py` passes, and each part fails without its
change:
  - default put back to 0: the "Insert, F1 or Esc" check fails;
  - migration line removed: `loaded_as(0, old) == 2` fails;
- `save()` left writing `options_version=4`: `loaded_as(mode, 4) ==
mode` fails (a choice made after the change would
    be moved again).
- The other tests that read the menu's sources, its options or the
window's Esc handling pass: `test_menu_env_on`,
`test_audio_output_menu`, `test_framegen_preset`, `test_rumble_menu`,
`test_vsync_row`, `test_audio_volume`,
`test_menu_size`, `test_stick_dead_zone`, `test_menu_hit`,
`test_menu_restart`, `test_menu_restart_slots`,
  `test_gpu_choice`, `test_display_pointer`, `test_screensaver`.
- `tests/run_all.py` on Windows (llvm-mingw), on main `e19d969` with
this change: 341 passed, 2 failed, 30 skipped.
The two failures fail the same way on untouched main on this PC:
`test_precompile_compute_guard` (this toolchain has no
`windows.h` for its fixture) and `test_recorder_sync` (its exit case;
its `vk_menu.cpp` source checks pass).

**Not tested**: in game (no build of this branch was run; the Esc
behaviour itself is #1998's, which was played),
Linux.

**Screenshots**: nothing new on screen. The row and its values are
#1998's; it now starts on "Insert, F1 or Esc".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic is right (coopmat_init() runs before the new call, and menu_env_on's version < since leaves Descriptor reuse alone), but the scope is wider than the description says:

  • "Without matrix hardware" covers RDNA2 and older, many Intel cards, and GTX 10/16 (vk_coopmat.cpp:342). NVIDIA is only unchanged for RTX. Existing users with options_version 4 who chose the game's upscaler are switched once.
  • The default is saved on the first row change, so a test run with RX_VK_MMA_SOFTWARE=1 on RTX makes FSR 3.1 permanent.
  • It's never reconsidered after a GPU upgrade.
  • There are no before/after screenshots.
  • vk_fsr3.h is included twice in vk_menu.cpp.
  • It conflicts with main's vk_menu.cpp (options_version=4 at :1430, migration at :3113), vk_menu.h and vk_coopmat.cpp. options_version=5 is also used by #2119 and #2197.

Whether this should cover every non-matrix GPU or only Deck/RDNA2 is a maintainer call.

hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
…ters' motion (#2305)

AMD FSR 3.1 makes its own inputs (`vk_fsr3_inputs.cpp`), and the only
motion it gets is the camera's. So anything that
moves differently from the camera is given the motion of what's behind
it: Link while he walks, his swinging arm, his
shield, and the people around him. DLSS and FSR 4 already get those
characters' own motion on `vk_temporal.cpp`'s chain.
This PR puts FSR 3.1 on that chain too, as a third engine, so all three
temporal upscalers share the same inputs.

**What changed**
- `vk_fsr3.cpp/.h`: `kChainEngine`, FSR 3.1 as a `TemporalEngine` (slot
2).
- It takes the chain's colour, depth, motion (with the actor pass drawn
over it), jitter and jitter phase count.
  - It builds FSR's depth constants from the unjittered projection.
- Its picture is copied into the chain's output and squared back by
`dlss_out.comp` as before.
- `fsr3_chosen()` is "the row or `RX_VK_FSR=1` picks FSR 3.1".
`fsr3_wanted()` now means "FSR 3.1 on its own inputs",
    which is false while it's on the chain.
- `fsr3::prepare_sizes` and `fsr3::release` let the chain size and free
FSR's images.
- `vk_temporal.cpp/.h`:
- `temporal_mode` returns `FSR3` for the row's FSR 3.1 before it falls
back to the game's own; `engine_of` gets a third
    slot.
  - Two new `TemporalEngine` fields:
- `fsr_depth`: the chain reverses depth for FSR's passes (built with
`INVERTED_DEPTH`) where the projection puts the
      near plane at 0.
- `retry`: a failed evaluation hands that frame to the network instead
of turning the engine off for good. FSR 3.1's
constants come from the frame's stream, which can run out for one frame.
- `TemporalFrame` carries the unjittered projection and the jitter phase
count.
  - The chain's output image gets `TRANSFER_DST`.
  - `RX_VK_FSR_JITTER=0` still turns FSR 3.1's jitter off on the chain.
- `shaders/dlss_prep.comp`:
  - Flag bit 3 reverses depth (`1 - d`).
- A NaN in the scene colour is written as black, following
`fsr3_prep.comp`'s rule, because one NaN spreads through an
    upscaler's history.
- The header was regenerated with `glslangValidator -V --target-env
vulkan1.3 --vn DLSS_PREP_COMP_SPV`, the command
    `test_dlss_actor.py` checks.
- `RX_VK_FSR3_CHAIN=0` gives FSR 3.1 its own inputs, as before, for
comparisons (`docs/development.md`). The readme's FSR 3.1
section describes the new inputs, and says what still trails: things
that aren't skinned, such as Navi's body.
- Tests:
- `temporal_jitter_test.cpp` covers FSR 3.1's slot and its failure gate,
and a new run (`fsr-jitter-off`) where only
`RX_VK_FSR_JITTER=0` is set: FSR 3.1 on the chain gets no jitter, and
DLSS and FSR 4 keep theirs.
- `test_upscaler_rows.py` checks that the row's FSR 3.1 runs on the
chain before the fallback to the game's own, that
`fsr3_wanted` is off while it does, and that `engine_of` has its slot.
  - Both fail on main. A mutated jitter gate fails `fsr-jitter-off`.

**Testing**
- `python -I -B tests/run_all.py`: 363 passed, 0 failed, 11 skipped
(Linux; the skips are Windows-only, need a Vulkan 1.3
  device, or need dotnet).
- In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel
7.3.3), KDE Plasma Wayland, windowed, 2560x1440
  output, 60 fps cap.
- Host v2.1.0-main-local (source fc2ed0d0). Renderer: main 49f75dc plus
this branch.
- "Own" means the same build with `RX_VK_FSR3_CHAIN=0`, which is what
main does.
- Every chain run logged `vk: fsr3 on the chain: 600 frames drawn ...
depth reversed for FSR by the chain`, with no
    evaluation failures.

| FSR 3.1 | Kokiri Forest, 28 s camera turn (4 runs each) | Castle Town
market at noon (3 runs each) |
  |---|---|---|
  | Own inputs (as main) | 59.2 fps, 1% low 46.4 | 53.1 fps |
  | On the chain | 59.3 fps, 1% low 48.1 | 52.5 fps |

Market runs vary by about 0.9 fps between runs, so the chain costs
nothing measurable.
- Picture: consecutive 2560x1440 shots at Kokiri's lost-woods path,
standing and walking, compared on the same build.
Edge flicker is the mean frame-to-frame change of edge pixels, 0-255,
while standing.

| FSR 3.1 | Edge flicker, standing | 1-px outlines per MP | Halos per MP
|
  |---|---|---|---|
  | 100%, own | 2.17 | 32 | 45 |
  | 100%, chain | 2.20 | 29 | 50 |
  | Quality (67%), own | 3.01 | 12 | 25 |
  | Quality (67%), chain | 1.67 | 2 | 9 |

Four independent reviewers of the walking sheets saw no visible
difference at 100%. At 67%, they found the chain
slightly better: smoother edges on Link's shield and tunic hem, and
slightly softer shield texture. No new artefacts.
The clearest evidence is the motion field itself (below). On main, Link
isn't in it at all.

**Not tested:**
- other GPUs and drivers;
- Windows;
- Steam Deck, where #2110 would make FSR 3.1 the default;
- frame interpolation (`RX_VK_FSR_FG=1`), which keeps using
`vk_fsr3_inputs.cpp`;
- switching upscalers repeatedly while playing (release and remake are
code-reviewed, not exercised in game);
- HDR: the chain still clamps FSR 3.1's colour at the paper white, as
main does (see below).

**Screenshots** (from the game's output image before the present path,
2560x1440)

The motion vectors the upscaler gets while Link walks
(`RX_VK_DLSS_DEBUG=motion`). Left: camera motion only
(`RX_VK_DLSS_ACTORS=0`, which is what FSR 3.1 got on main). Right: on
the chain, with Link's body, arm and shield carrying
their own motion:

![motion
vectors](https://raw.githubusercontent.com/unixman69/Hookshot/pr-screenshots/fsr3-chain/motion-vectors.png)

Link walking: own inputs vs the chain, at 100% and at 67% (2x zoom
crops):


![walking](https://raw.githubusercontent.com/unixman69/Hookshot/pr-screenshots/fsr3-chain/link-walking-own-vs-chain.png)

**Merge notes** (from test merges against every open PR that touches
these files; textual conflicts show up in git, semantic ones don't)
- **Depth is reversed for FSR 3.1 on the chain.**
`TemporalFrame::depth_reversed` says so. Anything that reads the chain's
  depth must check it rather than assume plain depth:
- #2118 (DLSS frame generation through NGX, now on main): done in this
PR. Commit ca2e302 passes `depth_reversed` to
    NGX's `depthInverted`, and `test_upscaler_rows` checks it.
- #1944 (DLSS frame generation through Streamline) needs the same:
`depthInverted` from `depth_reversed`, not a
    hard-coded false.
- #1831/#1833: `nr_pre` passes `s.depth` on, and the NGX backend's
`DepthInverted` should follow it too.
- **#2145** adds `bool native_aa` to `TemporalEngine::prepare`.
`chain_prepare` in `vk_fsr3.cpp` needs the same
parameter, or the build breaks. git merges the two cleanly. FSR 3.1
never has native AA.
- **#1653** (reactive mask) is on main now and merged into this branch.
Commit 7ceae45 makes the mask in `chain_evaluate`
the same way DLSS does on the chain, and `test_reactive_mask.py` checks
it. Checked in game with `RX_VK_REACTIVE=1`: the mask is
  made, and FSR 3.1 draws all 600 frames on the chain.
- **#2243** (DLSS HDR, linear light) is on main now (in #2342's squash,
a0dfd28) and is merged into this branch.
- `dlss_prep.comp` scrubs NaN first, then applies #2243's
`max`/`kSquare`/`min`, and keeps the `flags & 8` depth flip.
The header was regenerated; glslang 16.4.0 gives the same SPIR-V words
as CI's 16.6.0 for `dlss_prep` and `dlss_out`.
- `TemporalEngine` is ordered `memory`, `hdr`, `fsr_depth`, `retry`:
DLSS's and FSR 4's engines are positional,
    and `kChainEngine` is designated.
- FSR 3.1 doesn't report `hdr` yet, so it keeps the LDR pair. Giving it
HDR colour on #2243's mechanism is a follow-up.
- **#1831**:
- It reads `fsr3_wanted()` to ask whether FSR 3.1 is chosen. After this
PR that question is `fsr3_chosen()`. Its
`fsr4 = !fsr3_wanted() && fsr4_wanted()` guard and its jitter guard
should use `fsr3_chosen()`. The merge is
    textually clean, so git won't flag it.
- In `vk_temporal.cpp`, the output image's usage should become
`TRANSFER_DST | TRANSFER_SRC`, and its `proj[16]` and
    this PR's `projection[16]` should become one field.
- **#2166** only matters with `RX_VK_FSR3_CHAIN=0`.
- `docs/tested-configurations.md` and `docs/development.md`: rows next
to other PRs' rows; keep both.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the rebase. The new upscaler_default flag is a nice answer to two of Joey-Magnus's earlier points: an RX_VK_MMA_SOFTWARE=1 test run on RTX no longer makes FSR 3.1 stick, and the default is worked out again on another GPU until the player touches the row. The duplicate vk_fsr3.h include is gone, and the options_version comment now covers the new step. I also checked the other ways the Upscaler can change: the launcher bridge goes through set_row, so it clears the flag too. Presets don't touch opts.dlss.

One thing breaks it, though:

  • blocking The call in create_device now runs before dev.max_push_descriptors is read (it's set in the if (dev.push_descriptor) block a few lines below, and nowhere else). When the matrices run in software and no Upscaler has been chosen, fsr3::supported() is asked while the limit is still 0. It fails the < 15 check, and because dev.device is already set it caches that false for the whole run. So on the Deck and on Intel, FSR 3.1 is never picked by default. Worse, it also drops out of the Upscaler row for that run, which is a regression from main. The description says the call runs after the limit is read, and Joey's review of the first version said the same, so I think the rebase moved it. Moving the call after the push-descriptor block (just before return true;) fixes it. Details inline.
  • should fix options_version=6 is also used by #2119 (still open). Whichever lands second needs to bump to 7 and move its version < N check with it.
  • The description still talks about options_version=5 throughout, and the tests in it were run before the rebase. Please update them, and re-run the Intel "no options file → FSR 3.1" case after the fix. It would have caught the problem above.
  • Still open from Joey's review: an in-game run on a Deck (or another RDNA2 card) with before/after screenshots and the log line, and a release-note line for players whose game-upscaler setting moves once. Whether this should apply to every GPU without matrix hardware (GTX 10/16 and older Intel too), or only to the Deck/RDNA2, is still @NotRexed's call.

CI has only run the title check so far, so the full build and tests haven't run on this head yet.

Comment thread gpu/vk/vk_main.cpp Outdated
if (compute_family != UINT32_MAX) vkGetDeviceQueue(dev.device, compute_family, 0, &dev.present_queue), dev.present_family = compute_family;
if (dev.buffer_marker) buffer_markers_init();
coopmat_init();
menu_upscaler_default(coopmat_software());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dev.max_push_descriptors is still 0 here: it's read in the if (dev.push_descriptor) block below. When the flag is set and the matrices run in software, fsr3::supported() logs "fewer than 15 (0)" and caches false for the run (its static const bool). That turns FSR 3.1 off on exactly the GPUs this targets, including in the menu. Moving this line after that block, just before return true;, fixes it.

hckrman101 added a commit that referenced this pull request Nov 11, 2026
…ape by default (#2119)

A player on an ultrawide, 16:10 or 4:3 screen starts with the game's
16:9 picture until they find the Graphics tab's **Ultrawide** row,
change it and restart. The name also hides that the same row shapes the
picture on screens narrower than 16:9. PC games usually follow the
monitor's shape from the first start, with 16:9 as a choice.

**What changed**
- The **Ultrawide** row is now **Aspect ratio**, and **Monitor's shape**
is its default (`gpu/vk/vk_menu.h`; `host/hos_main.c` reads an options
file with no `ultrawide=` line as Monitor's shape). 16:9 stays a choice.
On a 16:9 screen nothing changes.
- The saved key stays `ultrawide=` (`ini_key`), so a saved `ultrawide=0`
still means 16:9.
- **Older options files on a screen narrower than 16:9.** The game saves
every row, so a file from before this PR holds
`ultrawide=0`, the old default, whether chosen or not. On a screen
narrower than 16:9 (16:10 such as the Steam Deck's
1280x800, or 4:3), where 16:9 only adds bars above and below, that
file's 16:9 moves to Monitor's shape; an ultrawide keeps
its 16:9 (often a real choice there). The rule is one function,
`hos_aspect_on` in `host/hos_wide_size.h`: `host/hos_main.c`
starts the game with it, and the menu's row follows what the host
started with (`host->aspect_ratio()`, in `menu_init`
before `restart_init`), so the two agree on Linux too, where the menu
has no screen size of its own. `save()` writes
  `options_version=6`, so a 16:9 chosen after the update stays.
- The other value reads **16:9** again instead of #2078's "Off (16:9)".
#2078 added "Off" because "Ultrawide: 16:9" read as if ultrawide were
already on; under the name Aspect ratio the value is a shape, and
"Aspect ratio: Off (16:9)" would not read well. Happy to keep "Off
(16:9)" if you prefer it.
- `recomplauncher.json`: the same label and help, and its default is
`1`, so the launcher does not write 16:9 over the new default.
- New help text for the row, and for **Ultrawide menus**, whose text
named the old row. The readme's settings table and Output resolution
notes, `menu_restart.h` and the comments that named the row follow
(#2052's wording kept, with the new name).
- Tests: `test_menu_restart.py` and `test_menu_restart.cpp` use the new
name; `test_audio_output_menu.py` and `test_stick_dead_zone.py` give
their option stubs the `ultrawide` field that `ini_key` now reads. New
`test_aspect_default.py`: `hos_aspect_on` over 16:10, 4:3, 5:4, 16:9,
1366x768, 21:9, 32:9, portrait and no screen size, plus the host and
menu source checks.

**Tested**
- In game on Windows 11, RTX 5090 (driver 621.14), 5120x2160 (21:9)
monitor, in one build with the widescreen cap change from #2120: the
picture takes the monitor's shape, and Kokiri Forest and Castle Town
load. That run was before the merge with current main (#2052, #2041,
#2078). This PR only moves a default and renames a row; the code that
sizes the picture is unchanged.
- `tests/run_all.py` on Windows (llvm-mingw) after merging main at
06c30bd (with #2197): 392 passed, 31 skipped, 1 failed:
`test_vram_budget_row`, which fails the same way on main's own sources
on this PC (clang stops on the unused `kVramMinimumOnly`).
- A build of this branch compiled (`hos_main.c`, `vk_menu.cpp`) before
that merge; the merge itself only adds the version 6 move next to
#2197's. The move for older files was not run in game: this PC has a
21:9 screen, where an older 16:9 stays. The tests this PR touches pass,
and so do the others that build `ini_key` or the row:
`test_framegen_preset`, `test_menu_env_on`, `test_recomplauncher_json`,
`test_rumble_menu`, `test_upscaler_rows`, `test_vsync_row`,
`test_output_resolution`, `test_widescreen_shapes`.

**Not tested**
- 16:10, 4:3, 32:9 and 48:9 screens in game (`test_widescreen_shapes.py`
covers their sizes).
- Linux and the Steam Deck: SDL gives the screen's size there too, so a
Steam Deck (16:10) now starts with a 16:10 picture instead of 16:9.
- If #1864's translations name the row, they would need the new name.

**Screenshots**

From a headless run of this branch (head 2a74ec4) on Windows 11, RTX
5090, with a 5120x2160 (21:9) monitor and no options file, so a first
start (`HOS_HEADLESS=1`, `RX_VK_SHOTS` at 2560x1080; the save loaded
with `HOS_INPUT_SCRIPT` presses). The log: `widescreen: picture
3416x1440, scene 2560x1080, camera ratio 2.3722`.

![The Graphics tab: Aspect ratio at Monitor's shape, the default,
selected with its
help](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/aspect-2119/aspect-ratio-row.webp)

![First start on the 21:9 monitor: Kokiri Forest at the monitor's shape,
the HUD in the middle
16:9](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/aspect-2119/first-start-21x9-kokiri.webp)

_The menu shot is the middle 16:9 of the frame: behind it the title
movie stays 16:9 with black sides. Its build had one extra line so the
list scrolls headless (`ease_step = 1.0f` in `menu_record_image`), not
part of this PR._

The widescreen cap PR (#2120) edited the same readme paragraph; it has
merged, and the paragraph is merged with it here.

**`options_version`:** #2197 (merged) took 5 for the Esc default, so
this writes 6 and moves an older file's 16:9 from files
before 6 (`version < 6` in `hos_aspect_on` and `menu_init`). #2110 also
writes 5 today; whichever of the two lands later
takes the next number. #2197's `test_menu_keys.py` compiles
`menu_init`'s options read, which now also reads
`host->aspect_ratio`, so its `host` stub gains that member. Its check of
older files expected every version below the one
`save()` writes to move Insert or F1 to Insert, F1 or Esc, which would
move a version 5 file's chosen Insert or F1 again; it
now reads the move's version from `menu_init` and checks that files from
it up to `save()`'s version keep their choice.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: hckrman101 <239247847+hckrman101@users.noreply.github.com>
On a GPU without cooperative-matrix hardware (AMD RDNA2 such as the
Steam Deck, Intel), the game's own upscaler runs its network's matrix
multiplies in a shader function: about 12 ms of a Steam Deck frame for
that one dispatch (NotRexed#1000), which FSR 3.1 replaces. There the Upscaler
now defaults to AMD FSR 3.1 when FSR 3.1 runs; with matrix hardware the
game's own stays. The default is settled at the end of create_device,
after the push-descriptor limit fsr3::supported() reads.

Until the player moves the Upscaler row, the file keeps
upscaler_default=1 and each start settles the default again, so another
GPU, or a run with RX_VK_MMA_SOFTWARE=1, gets its own. A file from
before options_version 7 with the game's own Upscaler wrote it whether
chosen or not, so it is treated as not chosen, as Descriptor reuse was
at version 4. tests/test_upscaler_default.py checks the order and the
flag.

Release note: on the Steam Deck and other GPUs without matrix hardware,
the Upscaler now starts on AMD FSR 3.1. If you had the game's own
upscaler selected, it moves to FSR 3.1 once; choose it again in
Graphics > Upscaler and it stays.

Assisted-by: Claude Opus 5.5 (Anthropic)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@addisonbair

Copy link
Copy Markdown
Contributor Author

Closing this one: on current main it makes the Steam Deck slower, the opposite of what it was for.

I did the Deck check you asked for, and also measured what each upscaler costs. Setup: main (eaef146 game, this PR's renderer for both rows, only the Upscaler row changed), the first-start Minimum preset, the docked profile (what a first start sets), the standing Kokiri scene at a 1200 MHz GPU clock, two passes each:

Deck (Van Gogh, RADV) the game's own FSR 3.1 FSR's extra GPU
2048x1152 output 12.4 / 12.3 fps, GPU 39.5 ms 11.2 / 11.2 fps, GPU 47.5 ms +8.0 ms
1280x800 output 13.2 / 13.2 fps, GPU 34.5 ms 12.2 / 12.2 fps, GPU 38.5 ms +4.0 ms

The handheld profile shows the same thing (2048x1152: 14.1 vs 13.2 fps, +5 ms GPU).

The default itself works on the Deck. "Upscaler AMD FSR 3.1 by default" is logged on a first start, again on the next start from the file it wrote, and from a file main wrote. Intel ARL behaves the same in start-up checks. The pictures are equivalent.

The reason the premise no longer holds: the ~12 ms this was meant to save was the game's TAAU network on v2.0.14, where FSR 3.1 really was a win on the Deck (#1000). On current main, AMD runs a cheap bilinear stand-in in its place (the context of #1439), so on these GPUs FSR 3.1 only adds cost. For image quality on AMD, #1439 (FSR 1 EASU + RCAS over the stand-in, ~0.5 ms) looks like the better path. I couldn't measure Intel in-game here (pipelines fail with -13 on this ARL iGPU), so I have no evidence to narrow it to Intel either.

Thanks for the careful reviews. The ordering bug you caught would have been a real regression.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants