mirror of
https://github.com/SpeedBreakerProject/speedbreaker.git
synced 2026-10-18 19:01:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad14799d14 |
No files matched your search
@@ -4,6 +4,38 @@ What changed in each SpeedBreaker release, newest first. Each release's notes on
|
||||
its entry here, and the game shows the entry once, the first time a new version starts (offline:
|
||||
it's built into the game).
|
||||
|
||||
## v0.1.5
|
||||
|
||||
### What's new
|
||||
|
||||
- Settings > Controls: every keyboard control can be reassigned, and each control has two keys.
|
||||
Choose a row, Left or Right for its first or second key, press A or Enter, then the new key; Y
|
||||
clears a key, X puts a row's defaults back, Reset page the whole table. The default keys are
|
||||
unchanged (the arrows steer as well as A and D), and the left stick click gets a key of its own,
|
||||
Z. Thanks to love-gamer, who wrote it (GitHub pull request #12).
|
||||
- Settings > Graphics > Upscaling has a third choice, FSR 1 (AMD FidelityFX Super Resolution 1),
|
||||
for a window much larger than the Internal Resolution (1x on a 1440p, MacBook or 4K screen):
|
||||
curves, wires and diagonals stay smooth where bicubic shows steps, such as lamp posts against
|
||||
the sky or the speedometer's needle. It is used only when the picture is enlarged. Bicubic stays
|
||||
the default. Not on iPhone and iPad.
|
||||
- The race results screen no longer shows a thin strip of the race above and below it. An Xbox 360
|
||||
draws that strip too, and a TV of the time hid it past the edge of its screen; today's screens
|
||||
show the whole picture, so it is now black, on every screen shape (Steam Deck, 16:9 and others).
|
||||
|
||||
### More about it
|
||||
|
||||
- The Controls page: the keys are saved by name in settings.toml's `[keys]` table (`w`,
|
||||
`left_shift`, `none`). F1, Esc (it cancels a wait) and the Cmd and Windows keys can't be
|
||||
assigned. If you go back to v0.1.4 and change a setting there, it forgets the Controls page's
|
||||
keys.
|
||||
- FSR 1: Sharpening sets its sharpening pass too (0.3 of its full strength by default). Bicubic
|
||||
stays the default because FSR 1 makes foliage look a little painted, costs more GPU time (about
|
||||
1 ms a frame on an M1 Pro at 1080p, 0.1 ms on a Steam Machine), and with Internal Resolution on
|
||||
Auto most screens aren't enlarged at all.
|
||||
- Updating keeps your saves, settings and the installed game, and needs no disc image: on a Mac,
|
||||
drag the new app into Applications and choose Replace, then Open Anyway once more; on SteamOS,
|
||||
unpack the new download over the old folder.
|
||||
|
||||
## v0.1.4
|
||||
|
||||
### What's new
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ endif()
|
||||
# apps' versions, the SteamOS bundles' VERSION file and the release tag
|
||||
# (v<version>). CHANGELOG.md has an entry for each version (the in-game
|
||||
# "What's new" note is built from it).
|
||||
project(SpeedBreaker VERSION 0.1.4 LANGUAGES C CXX)
|
||||
project(SpeedBreaker VERSION 0.1.5 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
AMD FidelityFX Super Resolution 1 (FSR 1): ffx_a.h and ffx_fsr1.h
|
||||
https://github.com/GPUOpen-Effects/FidelityFX-FSR (commit a21ffb8f6c13233ba336352bdff293894c706575, files ffx-fsr/ffx_a.h and ffx-fsr/ffx_fsr1.h)
|
||||
Vendored unmodified in runtime/thirdparty/fsr1; their shader code is compiled into SpeedBreaker.
|
||||
|
||||
Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -71,12 +71,12 @@ and a settings menu you can open mid-race.
|
||||
on the console.
|
||||
- **The original look:** sunlit roads and shadows as on the console, the picture a real Xbox 360
|
||||
sends to a TV (the game's own display gamma), correct colours on Apple devices, proper car paint
|
||||
and mirror reflections, and distant textures that don't shimmer. Anti-aliasing, sharpening, a
|
||||
Brightness setting and up to 16x anisotropic filtering are also available.
|
||||
and mirror reflections, and distant textures that don't shimmer. Anti-aliasing, sharpening,
|
||||
FSR 1 upscaling, a Brightness setting and up to 16x anisotropic filtering are also available.
|
||||
- **A guided installer.** It finds your disc image, checks that it is the right game, and
|
||||
verifies all 49 files as it copies them. An interrupted install never leaves a broken game.
|
||||
- **Your controller, or the keyboard.** Controllers work as an Xbox 360 pad, with hot-plugging
|
||||
and rumble support.
|
||||
and rumble support. Every keyboard control can be reassigned in the settings.
|
||||
- **A settings menu that pauses the game.** Open it with F1, Back + Start, or a three-finger
|
||||
tap. Every change applies at once, and *Quit Game* leaves the game from a controller too.
|
||||
- **Suspend and resume on iPhone and iPad.** Switch away and the game freezes; come back and it
|
||||
@@ -346,7 +346,7 @@ is B, Square is X and Triangle is Y.
|
||||
|
||||
| Xbox 360 control | Controller | Keyboard |
|
||||
|---|---|---|
|
||||
| Left stick | Left stick | A / D steer (the arrow keys also move it) |
|
||||
| Left stick | Left stick | A / D or Left / Right steer; Up / Down push it up and down |
|
||||
| Right trigger | Right trigger | W |
|
||||
| Left trigger | Left trigger | S |
|
||||
| A / B / X / Y | Bottom / right / left / top face button | Space / Backspace / Left Shift / Left Ctrl |
|
||||
@@ -355,7 +355,7 @@ is B, Square is X and Triangle is Y.
|
||||
| Start | Start / Menu / Options | Enter |
|
||||
| Back | Back / View / Create | Esc |
|
||||
| Right stick | Right stick | O / L up and down, K / ; left and right |
|
||||
| Stick clicks | Press a stick in | C (right) |
|
||||
| Stick clicks | Press a stick in | Z (left), C (right) |
|
||||
|
||||
- One player, one controller: the first one connected is used, and you can plug one in at any
|
||||
time. The keyboard always works as well.
|
||||
@@ -363,7 +363,11 @@ is B, Square is X and Triangle is Y.
|
||||
you let go, a hold after a quarter of a second.
|
||||
- The game shifts gears with the right stick, so from the keyboard you shift with O and L.
|
||||
Holding both keys of a pair centres the stick.
|
||||
- There is no button remapping and no mouse control in game.
|
||||
- Every keyboard control can be reassigned: **Settings > Controls** (F1). Each control has two
|
||||
keys: choose a row, Left or Right for its first or second key, press A or Enter, then the key
|
||||
you want (Esc or B cancels). Y or Backspace clears a key, X or Delete puts the row's defaults
|
||||
back, and *Reset page* restores this whole table. A key may drive several controls, as the
|
||||
arrows do. There is no mouse control in game.
|
||||
|
||||
**To open the settings:**
|
||||
|
||||
@@ -408,8 +412,8 @@ then Yes, puts it back.
|
||||
| Option | Choices | Default |
|
||||
|---|---|---|
|
||||
| Anti-Aliasing | On / Off | On |
|
||||
| Upscaling | Bicubic / Bilinear | Bicubic |
|
||||
| Sharpening | 0.00 to 1.00 | 0.30 (0 turns it off) |
|
||||
| Upscaling | Bicubic / Bilinear / FSR 1 | Bicubic. FSR 1 (AMD FidelityFX Super Resolution 1) keeps curves, wires and diagonals smooth when the picture is enlarged a lot, such as 1x on a 1440p or 4K screen; it is used only when the picture is enlarged, costs a little more GPU time, and makes foliage look slightly painted. Not on iPhone and iPad. |
|
||||
| Sharpening | 0.00 to 1.00 | 0.30 (0 turns it off). With FSR 1, the share of its sharpening pass's full strength. |
|
||||
| Anisotropic Filtering | Auto / Off / 2x / 4x / 8x / 16x / Original | Auto: 16x on dedicated graphics cards and M-series iPads, 4x elsewhere. Original uses each texture's own setting, as on the 360. |
|
||||
| Mipmaps | On / Off | On, so distant textures don't shimmer |
|
||||
| Internal Resolution | Auto / 1x (720p) / 1.5x (1080p) / 2x (1440p) / 3x (2160p) | Auto: enough to cover your screen, within what the device can afford (whole scales only: 1x, 2x, 3x, or 3x2 on an ultrawide). On iPhone and iPad, Auto always picks 1x. |
|
||||
@@ -420,6 +424,10 @@ game while its window isn't in front.
|
||||
**Input:** Stick Deadzone (8%; raise it if the car steers by itself), Trigger Deadzone (2%), and
|
||||
Vibration (On).
|
||||
|
||||
**Controls:** the keyboard keys for each Xbox 360 control, two per control, as in the table under
|
||||
[Controls](#controls). The keys are saved by name in `settings.toml`'s `[keys]` table (`w`,
|
||||
`left_shift`, `keypad_1`, or `none`; `runtime/user/key_names.h` lists them).
|
||||
|
||||
**Advanced:** Asynchronous Shaders (On: an object is skipped for a frame or two while its shader
|
||||
compiles, instead of the game stuttering), Performance Overlay (Off: fps, frame times, and CPU
|
||||
and GPU time per frame), **Save Bug Report**, and **Check for Updates** (the only thing that goes
|
||||
@@ -550,7 +558,7 @@ SpeedBreaker folder instead: `data/`, `data/game` and `cache/`.
|
||||
2. A notice says where the zip file went: your **Desktop** (on SteamOS, pick it up in Desktop
|
||||
Mode, or in the Desktop app on a Steam Frame), the **SpeedBreaker** folder in the Files app on
|
||||
iPhone and iPad, or the data folder above when there is no Desktop. Its name says which
|
||||
version you were running, for example `speedbreaker-bug-report-v0.1.4-<date>_<time>.zip`.
|
||||
version you were running, for example `speedbreaker-bug-report-v0.1.5-<date>_<time>.zip`.
|
||||
3. Open an [issue][issues], attach the zip, and say what you were doing, what you expected, what
|
||||
happened, and roughly how long into the session it happened.
|
||||
|
||||
@@ -749,6 +757,7 @@ research.
|
||||
| [SDL3](https://www.libsdl.org/) | Windows, input, audio, app lifecycle | zlib |
|
||||
| [Dear ImGui](https://github.com/ocornut/imgui) (Omar Cornut) | The installer and settings UI | MIT |
|
||||
| [Roboto](https://fonts.google.com/specimen/Roboto) (Google) | The UI font | Apache-2.0 |
|
||||
| [FidelityFX Super Resolution 1](https://github.com/GPUOpen-Effects/FidelityFX-FSR) (AMD) | The FSR 1 Upscaling choice | MIT |
|
||||
| [o1heap](https://github.com/pavel-kirienko/o1heap) (Pavel Kirienko) | The guest heap allocator | MIT |
|
||||
| [glslang](https://github.com/KhronosGroup/glslang) and [SPIRV-Tools](https://github.com/KhronosGroup/SPIRV-Tools) (Khronos) | Shader compilation at runtime | BSD-3-Clause and others; Apache-2.0 |
|
||||
| [MoltenVK](https://github.com/KhronosGroup/MoltenVK) and the [Vulkan Loader](https://github.com/KhronosGroup/Vulkan-Loader) (Khronos) | Vulkan on Mac, iPhone and iPad | Apache-2.0 |
|
||||
|
||||
@@ -28,6 +28,7 @@ collected when the bundle is made), and the iPhone and iPad IPA in `SpeedBreaker
|
||||
| **Roboto** (Medium): Copyright 2011 Google Inc. | The menus' font, embedded in the program. Roboto is a trademark of Google. | Apache-2.0 | [`LICENSES/Apache-2.0.txt`](LICENSES/Apache-2.0.txt) |
|
||||
| **o1heap**: Copyright (c) 2020 Pavel Kirienko | The game's heap allocator (`runtime/thirdparty/o1heap`). | MIT | [`LICENSES/o1heap-MIT.txt`](LICENSES/o1heap-MIT.txt) |
|
||||
| **QR Code generator library** (C) 1.8.0: Copyright (c) 2022 Project Nayuki | The QR code of the download page in Settings > Advanced > Check for Updates (`runtime/thirdparty/qrcodegen`). | MIT | [`LICENSES/qrcodegen-MIT.txt`](LICENSES/qrcodegen-MIT.txt) |
|
||||
| **AMD FidelityFX Super Resolution 1** (FSR 1, `ffx_a.h` and `ffx_fsr1.h` at commit a21ffb8): Copyright (c) 2021 Advanced Micro Devices, Inc. | Settings > Graphics > Upscaling's FSR 1: its EASU and RCAS shader code, compiled into the game's shaders, and its constant setup (`runtime/thirdparty/fsr1`, unmodified). | MIT | [`LICENSES/FidelityFX-FSR-MIT.txt`](LICENSES/FidelityFX-FSR-MIT.txt) |
|
||||
| **FFmpeg** 7.1.1: the FFmpeg developers | The XMA1, XMA2 and WMA Pro audio decoders, linked statically. Built without any GPL parts, with one SpeedBreaker change: [`patches/ffmpeg/0001-xma-single-stream-no-holdback.patch`](patches/ffmpeg/0001-xma-single-stream-no-holdback.patch) (no sample hold-back for single-stream XMA, which the game's audio mixer depends on). Source: `https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.xz` plus that patch; [`scripts/build_ffmpeg_xma.sh`](scripts/build_ffmpeg_xma.sh) builds it. | LGPL-2.1-or-later | [`LICENSES/FFmpeg-LGPL-2.1.txt`](LICENSES/FFmpeg-LGPL-2.1.txt) |
|
||||
|
||||
## Libraries in the downloads
|
||||
|
||||
@@ -22,6 +22,9 @@ from you.
|
||||
buttons on either side of the screen).
|
||||
- The D-pad picks a row, left/right changes it, A selects, LB/RB switch
|
||||
pages, B closes. The game pauses while the menu is open.
|
||||
- The **Controls** page: choose a row, Left or Right for its first or second key, press A (or
|
||||
Enter), then the key you want; Esc or B cancels. Y (Backspace) clears a key, X (Delete) restores
|
||||
the row, *Reset page* the whole table.
|
||||
|
||||
## When something goes wrong
|
||||
|
||||
|
||||
@@ -79,6 +79,20 @@ target_link_libraries(imgui PUBLIC SDL3::SDL3 Vulkan::Vulkan)
|
||||
file(READ ${IMGUI}/fonts/Roboto-Medium.ttf UI_FONT_HEX HEX)
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," UI_FONT_BYTES "${UI_FONT_HEX}")
|
||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated/ui_font.inc "${UI_FONT_BYTES}\n")
|
||||
# AMD FidelityFX Super Resolution 1 (thirdparty/fsr1, vendored, unmodified):
|
||||
# the text of its two headers, compiled in (generated/fsr1_headers.inc) for
|
||||
# Upscaling's FSR 1 shaders, which glslang compiles while the game runs
|
||||
# (video/fsr1.cpp). An edit of either reconfigures.
|
||||
set(FSR1 ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/fsr1)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${FSR1}/ffx_a.h ${FSR1}/ffx_fsr1.h)
|
||||
set(fsr1_inc "// Generated from runtime/thirdparty/fsr1 by runtime/CMakeLists.txt. Do not edit.\n")
|
||||
foreach(name ffx_a ffx_fsr1)
|
||||
file(READ ${FSR1}/${name}.h fsr1_hex HEX)
|
||||
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," fsr1_bytes "${fsr1_hex}")
|
||||
string(APPEND fsr1_inc "constexpr unsigned char k_${name}[] = { ${fsr1_bytes}0 };\n")
|
||||
endforeach()
|
||||
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated/fsr1_headers.inc.new "${fsr1_inc}")
|
||||
configure_file(${CMAKE_CURRENT_BINARY_DIR}/generated/fsr1_headers.inc.new ${CMAKE_CURRENT_BINARY_DIR}/generated/fsr1_headers.inc COPYONLY)
|
||||
target_include_directories(SpeedBreaker PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/generated)
|
||||
# The "What's new" note (ui/whats_new.cpp): this version's entry in
|
||||
# CHANGELOG.md, from its "## v<version>" heading to the next "## " one,
|
||||
|
||||
+65
-18
@@ -316,14 +316,20 @@ namespace gpu::bars
|
||||
// "WINNER" polaroid over asphalt and leaves; the title's and Controls'
|
||||
// key art have the same shape) is one opaque textured quad of the front
|
||||
// end, centred, x +-1.104 and y +-0.981 of the 16:9 band. On a 16:9
|
||||
// screen it hides the world (a thin border of it shows at the top and
|
||||
// bottom, as on the console, where a TV's overscan hid it); past the band
|
||||
// (Hor+ beside it, Vert+ above and below it) the world rendered behind
|
||||
// it showed: the race's road and cars at the sides of the results on an
|
||||
// iPhone (2868x1320). What a 16:9 screen shows of it is kept, the art's
|
||||
// own pixels too; everything else past the band (what no 16:9 screen
|
||||
// shows) is filled black, as a 16:9 picture's letterbox. Thresholds in
|
||||
// band units. The nearest draws it must not take: the fades and dims
|
||||
// screen it hides the world but for a thin border at the top and bottom
|
||||
// (y 0.981 to 1: ~7 of 720 rows each), where the race behind it shows;
|
||||
// a real 360's own output has that border too (HDMI captures of the
|
||||
// results screen), and a TV's overscan hid it. Past the band (Hor+
|
||||
// beside it, Vert+ above and below it) the world rendered behind it
|
||||
// showed as well: the race's road and cars at the sides of the results
|
||||
// on an iPhone (2868x1320). The art's own pixels are all kept, past the
|
||||
// band too; everything else is filled black on every screen shape, 16:9
|
||||
// too: past the band (what no 16:9 screen shows, as a 16:9 picture's
|
||||
// letterbox) and, inside it, the rows between the picture's top or
|
||||
// bottom edge and the band's (what a period TV's overscan hid). The
|
||||
// title's and Controls' key art have black around them already, so their
|
||||
// fill changes nothing on screen. Thresholds in band units. The nearest
|
||||
// draws it must not take: the fades and dims
|
||||
// (+-1.29: flat, and the widen, stretch and cover rules' own), the pause
|
||||
// dim (C8 alpha) and its gradients, the vid cam's noise and dark overlays
|
||||
// (3C alpha, x +-0.70), the overlay at the intro's start (x +-1.104, B8
|
||||
@@ -366,13 +372,14 @@ namespace gpu::bars
|
||||
return true;
|
||||
}
|
||||
|
||||
// What a backdrop fills, inside the draw's scissor: what lies past the
|
||||
// band (the columns beside it under Hor+, the rows above and below it
|
||||
// under Vert+; at most one of the view scales is below 1) and outside
|
||||
// the backdrop's own box, up to kFillEdge: per side, the part beyond the
|
||||
// box and, where the box reaches into that side, the parts above and
|
||||
// below it (Hor+; left and right of it under Vert+). At most 6.
|
||||
inline uint32_t BackdropRects(const Box& b, const float ps[4], float hostW, float hostH, const Rect& scissor, float scaleX, float scaleY,
|
||||
// What a backdrop fills past the band, inside the draw's scissor: what
|
||||
// lies past it (the columns beside it under Hor+, the rows above and
|
||||
// below it under Vert+; at most one of the view scales is below 1) and
|
||||
// outside the backdrop's own box, up to kFillEdge: per side, the part
|
||||
// beyond the box and, where the box reaches into that side, the parts
|
||||
// above and below it (Hor+; left and right of it under Vert+). At most
|
||||
// 6; none at 16:9.
|
||||
inline uint32_t PastBandRects(const Box& b, const float ps[4], float hostW, float hostH, const Rect& scissor, float scaleX, float scaleY,
|
||||
Rect out[6])
|
||||
{
|
||||
uint32_t n = 0;
|
||||
@@ -410,6 +417,45 @@ namespace gpu::bars
|
||||
return n;
|
||||
}
|
||||
|
||||
// The overscan rows, inside the draw's scissor: inside the band, the
|
||||
// rows between the backdrop's top edge and the band's and between its
|
||||
// bottom edge and the band's (y +-0.981 to +-1 of the band for the
|
||||
// results picture), across the band's width. Where the band's edge is
|
||||
// the screen's (y at 16:9 and under Hor+, x at 16:9 and under Vert+)
|
||||
// they reach kFillEdge, as the fills past the band do (at 2x with
|
||||
// half-pixel centres the outermost row's centre lies past clip 1); where
|
||||
// it isn't, they stop at it and PastBandRects' rectangles go on from
|
||||
// there, meeting them on the same pixel edge (gpu::bars::Edge). None
|
||||
// where the box reaches the band's edge. At most 2.
|
||||
inline uint32_t OverscanRows(const Box& b, const float ps[4], float hostW, float hostH, const Rect& scissor, float scaleX, float scaleY,
|
||||
Rect out[2])
|
||||
{
|
||||
uint32_t n = 0;
|
||||
auto add = [&](float cx0, float cx1, float cy0, float cy1) {
|
||||
if (cx1 <= cx0 || cy1 <= cy0)
|
||||
return;
|
||||
const Rect c = Intersect(ClipToHost(cx0, cx1, cy0, cy1, ps, hostW, hostH), scissor);
|
||||
if (!Empty(c) && n < 2)
|
||||
out[n++] = c;
|
||||
};
|
||||
const float e = kFillEdge;
|
||||
const float bandX = scaleX < 1.0f ? scaleX : e, bandY = scaleY < 1.0f ? scaleY : e;
|
||||
add(-bandX, bandX, b.y1, bandY);
|
||||
add(-bandX, bandX, -bandY, b.y0);
|
||||
return n;
|
||||
}
|
||||
|
||||
// What a backdrop fills, inside the draw's scissor: everything outside
|
||||
// its box but the columns inside the band beside it (none for a picture
|
||||
// as wide as the band): PastBandRects' and then OverscanRows'
|
||||
// rectangles, which never overlap. At most 8.
|
||||
inline uint32_t BackdropRects(const Box& b, const float ps[4], float hostW, float hostH, const Rect& scissor, float scaleX, float scaleY,
|
||||
Rect out[8])
|
||||
{
|
||||
const uint32_t n = PastBandRects(b, ps, hostW, hostH, scissor, scaleX, scaleY, out);
|
||||
return n + OverscanRows(b, ps, hostW, hostH, scissor, scaleX, scaleY, out + n);
|
||||
}
|
||||
|
||||
// The posScale that stretches the quad's box over [-edge, edge] in clip
|
||||
// x and y (x' = ax x + bx, likewise y), folded in: NDC = x' ps0 + ps2 =
|
||||
// x (ax ps0) + (ps2 + bx ps0). Orientation is kept (ax, ay > 0) whatever
|
||||
@@ -459,7 +505,7 @@ namespace gpu::bars
|
||||
Box box{}; // the quad's, clip space
|
||||
uint32_t argb = 0; // its colour
|
||||
uint32_t rectCount = 0;
|
||||
Rect rects[6]{};
|
||||
Rect rects[8]{}; // a bar's 2, a black frame's 2, a backdrop's 8
|
||||
float posScale[4]{};
|
||||
};
|
||||
|
||||
@@ -482,13 +528,14 @@ namespace gpu::bars
|
||||
}
|
||||
|
||||
// The register checks for a backdrop, before any vertex is read:
|
||||
// Candidate's, but over a large t0 (a picture, not the white texture).
|
||||
// Candidate's, but over a large t0 (a picture, not the white texture),
|
||||
// at every screen shape (16:9 too: its overscan rows).
|
||||
inline bool BackdropCandidate(const Draw& d, const BackdropRule& rule = {})
|
||||
{
|
||||
const float* c = d.c;
|
||||
const bool yScale = d.frontEndY > 0.0f ? std::fabs(std::fabs(c[5]) - d.frontEndY) <= d.frontEndY * 1e-5f
|
||||
: d.frontEndY == 0.0f && d.scaleY >= 1.0f;
|
||||
return (d.scaleX < 1.0f || d.scaleY < 1.0f) && !d.occlusion && d.prim == 13 && d.count == 4 && d.frontEndShader && d.leftAlone &&
|
||||
return !d.occlusion && d.prim == 13 && d.count == 4 && d.frontEndShader && d.leftAlone &&
|
||||
c[3] == 0.0f && c[4] == 0.0f && c[6] == 0.0f && c[7] == 0.0f && yScale && (d.vte & 0xF) == 0xF &&
|
||||
(d.blend0 == 0x07060706u || d.blend0 == 0x00010001u) && d.t0Sampled && d.t0Width >= rule.minTexels && d.t0Height >= rule.minTexels;
|
||||
}
|
||||
|
||||
+130
-87
@@ -7833,7 +7833,105 @@ void main()
|
||||
// of the front end is clipped to the band (below).
|
||||
// Edge bars (gpu/edge_bars.h): the copies of a police vid-cam side
|
||||
// bar or the intro's black frame that fill the screen past the 16:9
|
||||
// band, drawn right after it.
|
||||
// band, drawn right after it; and the black around a full-band
|
||||
// backdrop (the results screen), cleared right after it, at 16:9 too.
|
||||
// NFSMW_BAR_FILL=0 turns all the edge fills off (an A/B switch).
|
||||
// planEdgeFills: the plan for a front-end draw (its registers, what
|
||||
// the rules below did, a reader for its ePolys): with `bars`,
|
||||
// PlanFill's bars and black frames first (wide and tall screens
|
||||
// only), then PlanBackdrop's full-band picture (every screen shape).
|
||||
// logEdgeFill: what a plan does, once per kind, screen shape and
|
||||
// outcome, in every log (a tester's too).
|
||||
static const bool barFill = [] { const char* v = std::getenv("NFSMW_BAR_FILL"); return !(v && v[0] == '0'); }();
|
||||
auto planEdgeFills = [&](const float* c, float feY, float aspectX, float aspectY, bool feShader, bool leftAlone, bool bars) {
|
||||
xenos::xe_gpu_texture_fetch_t t0;
|
||||
memcpy(&t0, &s_regs[XE_GPU_REG_SHADER_CONSTANT_FETCH_00_0], sizeof(t0));
|
||||
const gpu::bars::Draw draw{ .prim = uint32_t(initiator.prim_type),
|
||||
.count = count,
|
||||
.c = c,
|
||||
.frontEndY = feY,
|
||||
.scaleX = aspectX,
|
||||
.scaleY = aspectY,
|
||||
.frontEndShader = feShader,
|
||||
.leftAlone = leftAlone,
|
||||
.occlusion = zpdCount,
|
||||
.vte = vte.value,
|
||||
.blend0 = s_regs[XE_GPU_REG_RB_BLENDCONTROL0] & 0x1FFF1FFF,
|
||||
.t0Sampled = (s_ps->info.texture2DMask & 1) != 0,
|
||||
.t0Width = uint32_t(t0.size_2d.width) + 1,
|
||||
.t0Height = uint32_t(t0.size_2d.height) + 1,
|
||||
.posScale = { pc.posScale[0], pc.posScale[1], pc.posScale[2], pc.posScale[3] },
|
||||
.hostW = w * float(scaleX),
|
||||
.hostH = h * float(scaleY),
|
||||
.scissor = { scissor.offset.x, scissor.offset.y, scissor.offset.x + int32_t(scissor.extent.width),
|
||||
scissor.offset.y + int32_t(scissor.extent.height) } };
|
||||
const uint32_t* vf = &s_regs[XE_GPU_REG_SHADER_CONSTANT_FETCH_00_0 + 95 * 2];
|
||||
const uint32_t base = vf[0] & 0x1FFFFFFC, endian = vf[1] & 3;
|
||||
const uint32_t words = (vf[1] >> 2) & 0xFFFFFF, first = s_regs[XE_GPU_REG_VGT_INDX_OFFSET];
|
||||
auto read = [&](uint32_t k, uint32_t j, uint32_t& word) {
|
||||
const uint32_t v = src.Get(k) + first; // an ePoly: 8 words
|
||||
if ((v + 1) * 8 > words)
|
||||
return false;
|
||||
word = GpuSwap(LoadPhysical(base + (v * 8 + j) * 4), endian);
|
||||
return true;
|
||||
};
|
||||
gpu::bars::Plan plan = bars ? gpu::bars::PlanFill(draw, read) : gpu::bars::Plan{};
|
||||
// The results screen's backdrop (and any opaque picture of that
|
||||
// shape: the title's key art): what lies outside the picture,
|
||||
// past the band and in the band's overscan rows above and below
|
||||
// it, is cleared to black after it, where the world drawn behind
|
||||
// it showed (gpu/edge_bars.h).
|
||||
if (plan.fill == gpu::bars::Fill::None)
|
||||
plan = gpu::bars::PlanBackdrop(draw, read);
|
||||
return plan;
|
||||
};
|
||||
auto logEdgeFill = [&](const gpu::bars::Plan& barPlan, float aspectX, float aspectY) {
|
||||
if (barPlan.fill == gpu::bars::Fill::None)
|
||||
return;
|
||||
static const bool logFrontEnd = std::getenv("NFSMW_LOG_FRONTEND") != nullptr;
|
||||
// A match each frame skips building the line once its key is
|
||||
// logged.
|
||||
const uint32_t kind = uint32_t(barPlan.fill);
|
||||
const bool blackout = barPlan.fill == gpu::bars::Fill::Blackout;
|
||||
const bool backdrop = barPlan.fill == gpu::bars::Fill::Backdrop;
|
||||
const gpu::bars::Box& box = barPlan.box;
|
||||
const uint64_t key = (0x36ull << 56) | (uint64_t(kind) << 40) | (uint64_t(barPlan.rectCount) << 36) |
|
||||
(uint64_t(std::lround(aspectX * 4096.0f)) << 18) | uint64_t(std::lround(aspectY * 4096.0f));
|
||||
static uint64_t loggedKey[5] = { ~0ull, ~0ull, ~0ull, ~0ull, ~0ull };
|
||||
if (loggedKey[kind] != key)
|
||||
{
|
||||
loggedKey[kind] = key;
|
||||
char fills[128];
|
||||
if (barPlan.rectCount)
|
||||
snprintf(fills, sizeof(fills), "fills %u rectangle(s) %s, first [%d,%d)x[%d,%d) (host px)", barPlan.rectCount,
|
||||
backdrop ? "outside it (past the 16:9 band, and the band's overscan rows)" : "past the 16:9 band", barPlan.rects[0].x0,
|
||||
barPlan.rects[0].x1, barPlan.rects[0].y0, barPlan.rects[0].y1);
|
||||
else
|
||||
snprintf(fills, sizeof(fills), "%s", blackout || backdrop ? "fills nothing" : "reaches the screen edge: nothing to fill");
|
||||
LogOnce(key,
|
||||
"edge bars: frame %llu vs_%016llx ps_%016llx: %s (clip x %.3f..%.3f y %.3f..%.3f, colour %08X; view scale x %.4f y %.4f) %s",
|
||||
(unsigned long long)s_frame, (unsigned long long)s_vs->hash, (unsigned long long)s_ps->hash,
|
||||
backdrop ? "a full-band backdrop (black outside it)" : blackout ? "a black frame"
|
||||
: kind == uint32_t(gpu::bars::Fill::RightBar) ? "the right vid-cam bar" : "the left vid-cam bar",
|
||||
box.x0, box.x1, box.y0, box.y1, barPlan.argb, aspectX, aspectY, fills);
|
||||
}
|
||||
// NFSMW_LOG_FRONTEND=1: the first frame of each run of frames
|
||||
// with a match, bars and black frames apart (one run per vid-cam
|
||||
// scene and per cut; any other is a draw this should not take).
|
||||
static uint64_t lastBarFrame[3] = { ~0ull - 1, ~0ull - 1, ~0ull - 1 };
|
||||
uint64_t& last = lastBarFrame[backdrop ? 2 : blackout ? 1 : 0];
|
||||
if (logFrontEnd && s_frame != last && s_frame != last + 1)
|
||||
{
|
||||
xenos::xe_gpu_texture_fetch_t t0;
|
||||
memcpy(&t0, &s_regs[XE_GPU_REG_SHADER_CONSTANT_FETCH_00_0], sizeof(t0));
|
||||
fprintf(stderr, "[frontend] edge bars: matching from frame %llu: %s x %.3f..%.3f y %.3f..%.3f colour %08X blend %08X t0 %08X %ux%u, %u rect(s)\n",
|
||||
(unsigned long long)s_frame, backdrop ? "backdrop" : blackout ? "black frame" : kind == uint32_t(gpu::bars::Fill::RightBar) ? "right bar" : "left bar",
|
||||
box.x0, box.x1, box.y0, box.y1, barPlan.argb, s_regs[XE_GPU_REG_RB_BLENDCONTROL0] & 0x1FFF1FFF,
|
||||
(uint32_t(t0.base_address) << 12) & 0x1FFFFFFFu, uint32_t(t0.size_2d.width) + 1, uint32_t(t0.size_2d.height) + 1,
|
||||
barPlan.rectCount);
|
||||
}
|
||||
last = s_frame;
|
||||
};
|
||||
gpu::bars::Plan barPlan;
|
||||
if (const float s = game::AspectScaleX(), sv = game::AspectScaleY(); s < 1.0f || sv < 1.0f)
|
||||
{
|
||||
@@ -8135,94 +8233,12 @@ void main()
|
||||
// drawn again after itself, stretched over the frame and
|
||||
// scissored to (its half of) what lies outside the band:
|
||||
// nothing inside the band changes.
|
||||
// NFSMW_BAR_FILL=0 turns it off (an A/B switch).
|
||||
static const bool barFill = [] { const char* v = std::getenv("NFSMW_BAR_FILL"); return !(v && v[0] == '0'); }();
|
||||
// NFSMW_BAR_FILL=0 turns it off (an A/B switch: barFill).
|
||||
const uint32_t blend0 = s_regs[XE_GPU_REG_RB_BLENDCONTROL0] & 0x1FFF1FFF;
|
||||
if (barFill)
|
||||
{
|
||||
xenos::xe_gpu_texture_fetch_t t0;
|
||||
memcpy(&t0, &s_regs[XE_GPU_REG_SHADER_CONSTANT_FETCH_00_0], sizeof(t0));
|
||||
const gpu::bars::Draw draw{ .prim = prim,
|
||||
.count = count,
|
||||
.c = c,
|
||||
.frontEndY = feY,
|
||||
.scaleX = s,
|
||||
.scaleY = sv,
|
||||
.frontEndShader = feShader,
|
||||
.leftAlone = kX == 1.0f && kY == 1.0f && kCover == 1.0f && !clipped,
|
||||
.occlusion = zpdCount,
|
||||
.vte = vte.value,
|
||||
.blend0 = blend0,
|
||||
.t0Sampled = (s_ps->info.texture2DMask & 1) != 0,
|
||||
.t0Width = uint32_t(t0.size_2d.width) + 1,
|
||||
.t0Height = uint32_t(t0.size_2d.height) + 1,
|
||||
.posScale = { pc.posScale[0], pc.posScale[1], pc.posScale[2], pc.posScale[3] },
|
||||
.hostW = w * float(scaleX),
|
||||
.hostH = h * float(scaleY),
|
||||
.scissor = { scissor.offset.x, scissor.offset.y, scissor.offset.x + int32_t(scissor.extent.width),
|
||||
scissor.offset.y + int32_t(scissor.extent.height) } };
|
||||
const uint32_t base = vf[0] & 0x1FFFFFFC, endian = vf[1] & 3;
|
||||
const uint32_t words = (vf[1] >> 2) & 0xFFFFFF, first = s_regs[XE_GPU_REG_VGT_INDX_OFFSET];
|
||||
auto read = [&](uint32_t k, uint32_t j, uint32_t& word) {
|
||||
const uint32_t v = src.Get(k) + first; // an ePoly: 8 words
|
||||
if ((v + 1) * 8 > words)
|
||||
return false;
|
||||
word = GpuSwap(LoadPhysical(base + (v * 8 + j) * 4), endian);
|
||||
return true;
|
||||
};
|
||||
barPlan = gpu::bars::PlanFill(draw, read);
|
||||
// The results screen's backdrop (and any opaque
|
||||
// picture of that shape: the title's key art): what
|
||||
// lies past the band and outside the picture is
|
||||
// cleared to black after it, where the world drawn
|
||||
// behind it showed (gpu/edge_bars.h).
|
||||
if (barPlan.fill == gpu::bars::Fill::None)
|
||||
barPlan = gpu::bars::PlanBackdrop(draw, read);
|
||||
}
|
||||
if (barPlan.fill != gpu::bars::Fill::None)
|
||||
{
|
||||
// Once per kind, screen shape and outcome, in every
|
||||
// log (a tester's too); a match each frame skips
|
||||
// building the line once its key is logged.
|
||||
const uint32_t kind = uint32_t(barPlan.fill);
|
||||
const bool blackout = barPlan.fill == gpu::bars::Fill::Blackout;
|
||||
const bool backdrop = barPlan.fill == gpu::bars::Fill::Backdrop;
|
||||
const gpu::bars::Box& box = barPlan.box;
|
||||
const uint64_t key = (0x36ull << 56) | (uint64_t(kind) << 40) | (uint64_t(barPlan.rectCount) << 36) |
|
||||
(uint64_t(std::lround(s * 4096.0f)) << 18) | uint64_t(std::lround(sv * 4096.0f));
|
||||
static uint64_t loggedKey[5] = { ~0ull, ~0ull, ~0ull, ~0ull, ~0ull };
|
||||
if (loggedKey[kind] != key)
|
||||
{
|
||||
loggedKey[kind] = key;
|
||||
char fills[96];
|
||||
if (barPlan.rectCount)
|
||||
snprintf(fills, sizeof(fills), "fills %u rectangle(s) past the 16:9 band, first [%d,%d)x[%d,%d) (host px)",
|
||||
barPlan.rectCount, barPlan.rects[0].x0, barPlan.rects[0].x1, barPlan.rects[0].y0, barPlan.rects[0].y1);
|
||||
else
|
||||
snprintf(fills, sizeof(fills), "%s", blackout || backdrop ? "fills nothing" : "reaches the screen edge: nothing to fill");
|
||||
LogOnce(key,
|
||||
"edge bars: frame %llu vs_%016llx ps_%016llx: %s (clip x %.3f..%.3f y %.3f..%.3f, colour %08X; view scale x %.4f y %.4f) %s",
|
||||
(unsigned long long)s_frame, (unsigned long long)s_vs->hash, (unsigned long long)s_ps->hash,
|
||||
backdrop ? "a full-band backdrop (black past the band)" : blackout ? "a black frame"
|
||||
: kind == uint32_t(gpu::bars::Fill::RightBar) ? "the right vid-cam bar" : "the left vid-cam bar",
|
||||
box.x0, box.x1, box.y0, box.y1, barPlan.argb, s, sv, fills);
|
||||
}
|
||||
// NFSMW_LOG_FRONTEND=1: the first frame of each
|
||||
// run of frames with a match, bars and black frames
|
||||
// apart (one run per vid-cam scene and per cut; any
|
||||
// other is a draw this should not take).
|
||||
static uint64_t lastBarFrame[3] = { ~0ull - 1, ~0ull - 1, ~0ull - 1 };
|
||||
uint64_t& last = lastBarFrame[backdrop ? 2 : blackout ? 1 : 0];
|
||||
if (logFrontEnd && s_frame != last && s_frame != last + 1)
|
||||
{
|
||||
xenos::xe_gpu_texture_fetch_t t0;
|
||||
memcpy(&t0, &s_regs[XE_GPU_REG_SHADER_CONSTANT_FETCH_00_0], sizeof(t0));
|
||||
fprintf(stderr, "[frontend] edge bars: matching from frame %llu: %s x %.3f..%.3f y %.3f..%.3f colour %08X blend %08X t0 %08X %ux%u, %u rect(s)\n",
|
||||
(unsigned long long)s_frame, backdrop ? "backdrop" : blackout ? "black frame" : kind == uint32_t(gpu::bars::Fill::RightBar) ? "right bar" : "left bar",
|
||||
box.x0, box.x1, box.y0, box.y1, barPlan.argb, blend0, (uint32_t(t0.base_address) << 12) & 0x1FFFFFFFu,
|
||||
uint32_t(t0.size_2d.width) + 1, uint32_t(t0.size_2d.height) + 1, barPlan.rectCount);
|
||||
}
|
||||
last = s_frame;
|
||||
barPlan = planEdgeFills(c, feY, s, sv, feShader, kX == 1.0f && kY == 1.0f && kCover == 1.0f && !clipped, true);
|
||||
logEdgeFill(barPlan, s, sv);
|
||||
}
|
||||
// The edge candidates (above) that still stop short of
|
||||
// the screen's edge after the rules, each distinct line
|
||||
@@ -8361,6 +8377,33 @@ void main()
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (barFill)
|
||||
{
|
||||
// 16:9 (or a 16:9 picture letterboxed on another shape): none of
|
||||
// the rules above has anything to do, and no bar or black frame
|
||||
// needs a fill, but a full-band backdrop (the results screen)
|
||||
// leaves the race showing in the band's overscan rows above and
|
||||
// below it: those are cleared to black (gpu::bars::OverscanRows).
|
||||
// A front-end draw as above (the front-end matrix in c0..c3,
|
||||
// ePolys at fetch constant 95, read by the vertex shader); nothing
|
||||
// is measured, and only one quad over a large t0 gets as far as
|
||||
// reading its vertices (gpu::bars::BackdropCandidate).
|
||||
if (const float feX = game::FrontEndScaleX(); feX > 0.0f)
|
||||
{
|
||||
float c[16];
|
||||
memcpy(c, &s_regs[XE_GPU_REG_SHADER_CONSTANT_000_X], sizeof(c));
|
||||
const uint32_t* vf = &s_regs[XE_GPU_REG_SHADER_CONSTANT_FETCH_00_0 + 95 * 2];
|
||||
if (std::fabs(c[0] - feX) <= feX * 1e-5f && c[1] == 0.0f && c[2] == 0.0f &&
|
||||
c[12] == 0.0f && c[13] == 0.0f && c[14] == 0.0f && c[15] == 1.0f && ((vf[0] & 3) == 3))
|
||||
{
|
||||
const auto& vi = s_vs->info;
|
||||
const bool feShader = ((vi.vertexFetchConstants[95 / 32] >> (95 % 32)) & 1) &&
|
||||
(vi.constRelative || (vi.constUsed[0] & 0xFull) == 0xFull);
|
||||
barPlan = planEdgeFills(c, game::FrontEndScaleY(), s, sv, feShader, true, false);
|
||||
logEdgeFill(barPlan, s, sv);
|
||||
}
|
||||
}
|
||||
}
|
||||
// NFSMW_LOG_DRAWS=<seconds>: every draw of one frame (target, viewport,
|
||||
// scissor, textures, shaders), consecutive duplicates folded.
|
||||
if (s_logDrawsNow)
|
||||
@@ -8600,7 +8643,7 @@ void main()
|
||||
ca[attachments].clearValue.color = { { 0.0f, 0.0f, 0.0f, 1.0f } };
|
||||
attachments++;
|
||||
}
|
||||
VkClearRect cr[6]{};
|
||||
VkClearRect cr[std::extent_v<decltype(gpu::bars::Plan::rects)>]{};
|
||||
for (uint32_t i = 0; i < barPlan.rectCount; i++)
|
||||
{
|
||||
const gpu::bars::Rect& r = barPlan.rects[i];
|
||||
|
||||
+37
-29
@@ -1,13 +1,30 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See input.h.
|
||||
#include <stdafx.h>
|
||||
#include "input.h"
|
||||
#include "key_map.h"
|
||||
#include <cpu/guest_time.h>
|
||||
#include <kernel/function.h>
|
||||
#include <ui/ui.h>
|
||||
#include <user/settings.h>
|
||||
#include <user/key_names.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
// key_map.h names XInput's bits as literals (it has no kernel header): keep
|
||||
// them the bits the game reads.
|
||||
static_assert(hid::keymap::button::DpadUp == XAMINPUT_GAMEPAD_DPAD_UP && hid::keymap::button::DpadDown == XAMINPUT_GAMEPAD_DPAD_DOWN &&
|
||||
hid::keymap::button::DpadLeft == XAMINPUT_GAMEPAD_DPAD_LEFT && hid::keymap::button::DpadRight == XAMINPUT_GAMEPAD_DPAD_RIGHT &&
|
||||
hid::keymap::button::Start == XAMINPUT_GAMEPAD_START && hid::keymap::button::Back == XAMINPUT_GAMEPAD_BACK &&
|
||||
hid::keymap::button::LeftThumb == XAMINPUT_GAMEPAD_LEFT_THUMB && hid::keymap::button::RightThumb == XAMINPUT_GAMEPAD_RIGHT_THUMB &&
|
||||
hid::keymap::button::LeftShoulder == XAMINPUT_GAMEPAD_LEFT_SHOULDER && hid::keymap::button::RightShoulder == XAMINPUT_GAMEPAD_RIGHT_SHOULDER &&
|
||||
hid::keymap::button::A == XAMINPUT_GAMEPAD_A && hid::keymap::button::B == XAMINPUT_GAMEPAD_B &&
|
||||
hid::keymap::button::X == XAMINPUT_GAMEPAD_X && hid::keymap::button::Y == XAMINPUT_GAMEPAD_Y,
|
||||
"key_map.h's button bits are XInput's");
|
||||
// The key codes of user/key_names.h are SDL's scancodes (USB HID usages);
|
||||
// tests/key_names_test.cpp checks every row, this pins a few.
|
||||
static_assert(settings::keynames::Code("a") == SDL_SCANCODE_A && settings::keynames::Code("keypad_1") == SDL_SCANCODE_KP_1 &&
|
||||
settings::keynames::Code("right_alt") == SDL_SCANCODE_RALT && settings::keynames::Code("escape") == SDL_SCANCODE_ESCAPE);
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t ERROR_SUCCESS = 0;
|
||||
@@ -608,39 +625,30 @@ namespace hid
|
||||
}
|
||||
const float rawLt = lt, rawRt = rt;
|
||||
|
||||
// Keyboard (digital; overrides the analog values it touches).
|
||||
const bool* k = SDL_GetKeyboardState(nullptr);
|
||||
if (k)
|
||||
// Keyboard (digital; replaces the controller's value for what it
|
||||
// touches): each control's key is Settings > Controls (the Key
|
||||
// options), resolved by key_map.h. A key is a scancode SDL's state
|
||||
// array is indexed by; an out-of-range value (a hand edit beat
|
||||
// Load's checks) counts as not held.
|
||||
if (const bool* k = SDL_GetKeyboardState(nullptr))
|
||||
{
|
||||
static const std::pair<SDL_Scancode, uint16_t> keys[] = {
|
||||
{ SDL_SCANCODE_UP, XAMINPUT_GAMEPAD_DPAD_UP }, { SDL_SCANCODE_DOWN, XAMINPUT_GAMEPAD_DPAD_DOWN },
|
||||
{ SDL_SCANCODE_LEFT, XAMINPUT_GAMEPAD_DPAD_LEFT }, { SDL_SCANCODE_RIGHT, XAMINPUT_GAMEPAD_DPAD_RIGHT },
|
||||
{ SDL_SCANCODE_SPACE, XAMINPUT_GAMEPAD_A }, { SDL_SCANCODE_BACKSPACE, XAMINPUT_GAMEPAD_B },
|
||||
{ SDL_SCANCODE_LSHIFT, XAMINPUT_GAMEPAD_X }, { SDL_SCANCODE_LCTRL, XAMINPUT_GAMEPAD_Y },
|
||||
{ SDL_SCANCODE_Q, XAMINPUT_GAMEPAD_LEFT_SHOULDER }, { SDL_SCANCODE_E, XAMINPUT_GAMEPAD_RIGHT_SHOULDER },
|
||||
{ SDL_SCANCODE_RETURN, XAMINPUT_GAMEPAD_START }, { SDL_SCANCODE_ESCAPE, XAMINPUT_GAMEPAD_BACK },
|
||||
{ SDL_SCANCODE_C, XAMINPUT_GAMEPAD_RIGHT_THUMB },
|
||||
bool pressed[keymap::kControls] = {};
|
||||
auto down = [&](settings::Id id) {
|
||||
int code = settings::GetInt(id); // 0: no key
|
||||
return code > 0 && code < SDL_SCANCODE_COUNT && k[code];
|
||||
};
|
||||
for (auto& [key, bit] : keys)
|
||||
if (k[key])
|
||||
p.buttons |= bit;
|
||||
float kx = float(k[SDL_SCANCODE_RIGHT] || k[SDL_SCANCODE_D]) - float(k[SDL_SCANCODE_LEFT] || k[SDL_SCANCODE_A]);
|
||||
float ky = float(k[SDL_SCANCODE_UP]) - float(k[SDL_SCANCODE_DOWN]);
|
||||
if (kx != 0.0f)
|
||||
lx = kx;
|
||||
if (ky != 0.0f)
|
||||
ly = ky;
|
||||
if (k[SDL_SCANCODE_W])
|
||||
for (size_t i = 0; i < keymap::kControls; i++)
|
||||
pressed[i] = down(keymap::kEntries[i].id) || down(keymap::kEntries[i].id2);
|
||||
keymap::Resolved r = keymap::Resolve(pressed);
|
||||
p.buttons |= r.buttons;
|
||||
if (r.rightTrigger)
|
||||
rt = 1.0f;
|
||||
if (k[SDL_SCANCODE_S])
|
||||
if (r.leftTrigger)
|
||||
lt = 1.0f;
|
||||
// The right stick (the game shifts gears with it: drag races
|
||||
// need it). While either key of an axis is held the keyboard
|
||||
// has that axis; both keys centre it.
|
||||
if (k[SDL_SCANCODE_O] || k[SDL_SCANCODE_L])
|
||||
ry = float(k[SDL_SCANCODE_O]) - float(k[SDL_SCANCODE_L]);
|
||||
if (k[SDL_SCANCODE_K] || k[SDL_SCANCODE_SEMICOLON])
|
||||
rx = float(k[SDL_SCANCODE_SEMICOLON]) - float(k[SDL_SCANCODE_K]);
|
||||
float* axes[size_t(keymap::Axis::Count)] = { &lx, &ly, &rx, &ry }; // Axis order: LX, LY, RX, RY
|
||||
for (size_t a = 0; a < size_t(keymap::Axis::Count); a++)
|
||||
if (r.axisSet[a])
|
||||
*axes[a] = r.axis[a];
|
||||
}
|
||||
p.lx = ToShort(lx);
|
||||
p.ly = ToShort(ly);
|
||||
|
||||
+6
-4
@@ -8,10 +8,12 @@
|
||||
// sticks rest at exactly zero while small movements still ramp up
|
||||
// smoothly from the edge of the deadzone. Triggers get their own
|
||||
// (NFSMW_TRIGGER_DEADZONE, default 0.02).
|
||||
// - Keyboard: arrows = D-pad + left stick, W/S = gas/brake (RT/LT), A/D =
|
||||
// steer, Space = A, Backspace = B, Left Shift = X, Left Ctrl = Y,
|
||||
// Q/E = LB/RB, Enter = START, Esc = BACK, O/L and K/; = right stick
|
||||
// (up/down, left/right), C = right stick click.
|
||||
// - Keyboard: each Xbox 360 control has two keys (Settings > Controls;
|
||||
// the [keys] table of settings.toml; hid/key_map.h lists them).
|
||||
// Defaults: arrows = D-pad, A/D and Left/Right = steer, Up/Down = left
|
||||
// stick too, W/S = gas/brake (RT/LT), Space = A, Backspace = B,
|
||||
// Left Shift = X, Left Ctrl = Y, Q/E = LB/RB, Enter = START, Esc = BACK,
|
||||
// O/L/K/; = right stick, Z/C = the stick clicks. A key may drive several.
|
||||
// - NFSMW_INPUT_SCRIPT="12:START,15.5:A,30-90/4:DOWN,100~160:RT": taps
|
||||
// (150 ms) at seconds since launch, taps every `step` seconds over a
|
||||
// range, or inputs held over a range; RT/LT (triggers) and LL/LR/LU/LD
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// The keyboard as an Xbox 360 controller (Settings > Controls): the 24
|
||||
// controls a key can drive, in the page's order, each with the two settings
|
||||
// that hold its keys (user/settings.h, the [keys] table of settings.toml),
|
||||
// and how a set of held keys becomes buttons, triggers and stick axes.
|
||||
// Header only and SDL-free: hid/input.cpp samples the keys and feeds the
|
||||
// result to the pad; ui/settings_menu.cpp draws one row per entry;
|
||||
// tests/key_map_test.cpp checks the rules.
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
#include <user/settings.h>
|
||||
|
||||
namespace hid::keymap
|
||||
{
|
||||
enum class Control : uint8_t
|
||||
{
|
||||
RightTrigger, LeftTrigger,
|
||||
LeftStickLeft, LeftStickRight, LeftStickUp, LeftStickDown,
|
||||
A, B, X, Y, LB, RB,
|
||||
DpadUp, DpadDown, DpadLeft, DpadRight,
|
||||
Start, Back,
|
||||
RightStickUp, RightStickDown, RightStickLeft, RightStickRight,
|
||||
LeftStickClick, RightStickClick,
|
||||
Count
|
||||
};
|
||||
constexpr size_t kControls = size_t(Control::Count);
|
||||
|
||||
enum class Role : uint8_t { Button, LeftTrigger, RightTrigger, AxisNeg, AxisPos };
|
||||
enum class Axis : uint8_t { LX, LY, RX, RY, Count };
|
||||
|
||||
// XInput's button bits, as the game reads them (xbox.h's
|
||||
// XAMINPUT_GAMEPAD_*; input.cpp asserts they agree). Literals here so
|
||||
// this header needs no kernel header.
|
||||
namespace button
|
||||
{
|
||||
constexpr uint16_t DpadUp = 0x0001, DpadDown = 0x0002, DpadLeft = 0x0004, DpadRight = 0x0008;
|
||||
constexpr uint16_t Start = 0x0010, Back = 0x0020, LeftThumb = 0x0040, RightThumb = 0x0080;
|
||||
constexpr uint16_t LeftShoulder = 0x0100, RightShoulder = 0x0200;
|
||||
constexpr uint16_t A = 0x1000, B = 0x2000, X = 0x4000, Y = 0x8000;
|
||||
}
|
||||
|
||||
struct Entry
|
||||
{
|
||||
Control control;
|
||||
settings::Id id; // the option holding its first key
|
||||
settings::Id id2; // ...and its second (always id + 1)
|
||||
Role role;
|
||||
uint16_t button = 0; // Role::Button
|
||||
Axis axis = Axis::LX; // Role::AxisNeg, AxisPos
|
||||
};
|
||||
|
||||
constexpr Entry kEntries[kControls] = {
|
||||
{ Control::RightTrigger, settings::Id::KeyRightTrigger, settings::Id::KeyRightTrigger2, Role::RightTrigger },
|
||||
{ Control::LeftTrigger, settings::Id::KeyLeftTrigger, settings::Id::KeyLeftTrigger2, Role::LeftTrigger },
|
||||
{ Control::LeftStickLeft, settings::Id::KeyLeftStickLeft, settings::Id::KeyLeftStickLeft2, Role::AxisNeg, 0, Axis::LX },
|
||||
{ Control::LeftStickRight, settings::Id::KeyLeftStickRight, settings::Id::KeyLeftStickRight2, Role::AxisPos, 0, Axis::LX },
|
||||
{ Control::LeftStickUp, settings::Id::KeyLeftStickUp, settings::Id::KeyLeftStickUp2, Role::AxisPos, 0, Axis::LY },
|
||||
{ Control::LeftStickDown, settings::Id::KeyLeftStickDown, settings::Id::KeyLeftStickDown2, Role::AxisNeg, 0, Axis::LY },
|
||||
{ Control::A, settings::Id::KeyA, settings::Id::KeyA2, Role::Button, button::A },
|
||||
{ Control::B, settings::Id::KeyB, settings::Id::KeyB2, Role::Button, button::B },
|
||||
{ Control::X, settings::Id::KeyX, settings::Id::KeyX2, Role::Button, button::X },
|
||||
{ Control::Y, settings::Id::KeyY, settings::Id::KeyY2, Role::Button, button::Y },
|
||||
{ Control::LB, settings::Id::KeyLB, settings::Id::KeyLB2, Role::Button, button::LeftShoulder },
|
||||
{ Control::RB, settings::Id::KeyRB, settings::Id::KeyRB2, Role::Button, button::RightShoulder },
|
||||
{ Control::DpadUp, settings::Id::KeyDpadUp, settings::Id::KeyDpadUp2, Role::Button, button::DpadUp },
|
||||
{ Control::DpadDown, settings::Id::KeyDpadDown, settings::Id::KeyDpadDown2, Role::Button, button::DpadDown },
|
||||
{ Control::DpadLeft, settings::Id::KeyDpadLeft, settings::Id::KeyDpadLeft2, Role::Button, button::DpadLeft },
|
||||
{ Control::DpadRight, settings::Id::KeyDpadRight, settings::Id::KeyDpadRight2, Role::Button, button::DpadRight },
|
||||
{ Control::Start, settings::Id::KeyStart, settings::Id::KeyStart2, Role::Button, button::Start },
|
||||
{ Control::Back, settings::Id::KeyBack, settings::Id::KeyBack2, Role::Button, button::Back },
|
||||
{ Control::RightStickUp, settings::Id::KeyRightStickUp, settings::Id::KeyRightStickUp2, Role::AxisPos, 0, Axis::RY },
|
||||
{ Control::RightStickDown, settings::Id::KeyRightStickDown, settings::Id::KeyRightStickDown2, Role::AxisNeg, 0, Axis::RY },
|
||||
{ Control::RightStickLeft, settings::Id::KeyRightStickLeft, settings::Id::KeyRightStickLeft2, Role::AxisNeg, 0, Axis::RX },
|
||||
{ Control::RightStickRight, settings::Id::KeyRightStickRight, settings::Id::KeyRightStickRight2, Role::AxisPos, 0, Axis::RX },
|
||||
{ Control::LeftStickClick, settings::Id::KeyLeftStickClick, settings::Id::KeyLeftStickClick2, Role::Button, button::LeftThumb },
|
||||
{ Control::RightStickClick, settings::Id::KeyRightStickClick, settings::Id::KeyRightStickClick2, Role::Button, button::RightThumb },
|
||||
};
|
||||
|
||||
consteval bool EntriesInOrder()
|
||||
{
|
||||
for (size_t i = 0; i < kControls; i++)
|
||||
if (size_t(kEntries[i].control) != i || size_t(kEntries[i].id) != size_t(settings::Id::KeyRightTrigger) + 2 * i ||
|
||||
size_t(kEntries[i].id2) != size_t(kEntries[i].id) + 1)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
static_assert(EntriesInOrder(), "one Entry per Control, in Control order, over the Key options in pairs");
|
||||
|
||||
// The control either of a Key option's keys belongs to.
|
||||
constexpr const Entry& EntryOf(settings::Id id)
|
||||
{
|
||||
return kEntries[(size_t(id) - size_t(settings::Id::KeyRightTrigger)) / 2];
|
||||
}
|
||||
|
||||
// What the held keys amount to.
|
||||
struct Resolved
|
||||
{
|
||||
uint16_t buttons = 0;
|
||||
bool leftTrigger = false, rightTrigger = false; // a trigger key is down: that trigger at full
|
||||
bool axisSet[size_t(Axis::Count)] = {}; // either key of the axis is down: the keyboard has the axis
|
||||
float axis[size_t(Axis::Count)] = {}; // +1, 0 (both keys) or -1 while set; up is positive, as XInput has it
|
||||
};
|
||||
|
||||
// pressed[i]: either key bound to Control(i) is down. Buttons OR in; an axis
|
||||
// is its positive key minus its negative one, and belongs to the
|
||||
// keyboard while either is held (both held: centred).
|
||||
constexpr Resolved Resolve(std::span<const bool, kControls> pressed)
|
||||
{
|
||||
Resolved r;
|
||||
for (size_t i = 0; i < kControls; i++)
|
||||
{
|
||||
if (!pressed[i])
|
||||
continue;
|
||||
const Entry& e = kEntries[i];
|
||||
switch (e.role)
|
||||
{
|
||||
case Role::Button:
|
||||
r.buttons |= e.button;
|
||||
break;
|
||||
case Role::LeftTrigger:
|
||||
r.leftTrigger = true;
|
||||
break;
|
||||
case Role::RightTrigger:
|
||||
r.rightTrigger = true;
|
||||
break;
|
||||
case Role::AxisNeg:
|
||||
r.axisSet[size_t(e.axis)] = true;
|
||||
r.axis[size_t(e.axis)] -= 1.0f;
|
||||
break;
|
||||
case Role::AxisPos:
|
||||
r.axisSet[size_t(e.axis)] = true;
|
||||
r.axis[size_t(e.axis)] += 1.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2021 Advanced Micro Devices, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
# AMD FidelityFX Super Resolution 1 (vendored)
|
||||
|
||||
`ffx_a.h` and `ffx_fsr1.h` from https://github.com/GPUOpen-Effects/FidelityFX-FSR at commit
|
||||
a21ffb8f6c13233ba336352bdff293894c706575 (2021-11-15; its `ffx-fsr/` folder), unmodified, MIT
|
||||
license (`LICENSE.txt`, the repository's `license.txt` as it is; also
|
||||
`LICENSES/FidelityFX-FSR-MIT.txt`).
|
||||
|
||||
SHA-256:
|
||||
|
||||
f60e2722fcd13989523b9164d776ab382b3692791767f3bf8bb19967f763f3fb ffx_a.h
|
||||
93c3922362ea7fc99cbcc698ca30c98de4f8c246d1fbb0b09e015ddef38ce3a5 ffx_fsr1.h
|
||||
db089274ce766da70f5b7d791029c3486f9f9e27c8c79c652689603d3192e802 LICENSE.txt
|
||||
|
||||
Used by Settings > Graphics > Upscaling's FSR 1 (`runtime/video/fsr1.cpp`): their 32-bit GLSL
|
||||
EASU and RCAS functions are compiled into the presenter's shaders (the build embeds both files'
|
||||
text, `generated/fsr1_headers.inc`), and their CPU code computes the shaders' constants.
|
||||
Vendored
+2656
File diff suppressed because it is too large.
Load diff
Vendored
+1199
File diff suppressed because it is too large.
Load diff
+327
-10
@@ -15,11 +15,14 @@
|
||||
|
||||
#include <report/report.h>
|
||||
#include <user/settings.h>
|
||||
#include <hid/key_map.h> // the Controls page: one row per control, two keys each
|
||||
#include <user/key_names.h> // a key's name in the file, when SDL has none
|
||||
#include <video/frame_rate.h>
|
||||
#include <video/presenter.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h> // ClosePopupToLevel: a dialog left open when the menu closed
|
||||
#include <SDL3/SDL_keyboard.h> // a key row's label on this layout
|
||||
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
@@ -39,6 +42,22 @@ namespace ui
|
||||
bool s_describedBugReport = false; // ...or Save Bug Report's
|
||||
bool s_describedUpdate = false; // ...or Check for Updates'
|
||||
bool s_opened = true; // just opened: take focus
|
||||
// The Controls page (KeyRow). A row's two keys are cells; the chosen
|
||||
// cell is the focused row's, and a wait is for one option of one
|
||||
// row (SettingsMenuTakesKey takes the press before ImGui sees it).
|
||||
Id s_waiting = Id::Count; // the Key option a row waits for; Count: no wait
|
||||
SDL_Scancode s_refused = SDL_SCANCODE_UNKNOWN; // a press the wait wouldn't take; the help line says so
|
||||
SDL_Scancode s_takenKey = SDL_SCANCODE_UNKNOWN; // the key a wait took or refused, until its release
|
||||
Id s_slotRow = Id::Count; // the row whose chosen cell s_slot is
|
||||
int s_slot = 0; // 0: the row's first key, 1: its second
|
||||
constexpr const char* kPressAKey = "Press a key";
|
||||
constexpr const char* kNoKey = "none";
|
||||
|
||||
void EndWait()
|
||||
{
|
||||
s_waiting = Id::Count;
|
||||
s_refused = SDL_SCANCODE_UNKNOWN;
|
||||
}
|
||||
|
||||
constexpr int kCategories = int(Category::Count);
|
||||
|
||||
@@ -62,6 +81,80 @@ namespace ui
|
||||
return delta;
|
||||
}
|
||||
|
||||
// iPhone and iPad: the Controls page only while a keyboard is
|
||||
// connected (nothing to bind without one).
|
||||
bool PageShown(Category c)
|
||||
{
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS
|
||||
if (c == Category::Controls)
|
||||
return SDL_HasKeyboard();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// A key's name for a cell: on this keyboard's layout ("Z" on a
|
||||
// French Mac for the position the file calls "w"), else SDL's name
|
||||
// of the position (the platform's: "Left Option" on a Mac), else the
|
||||
// file's. "none" for no key. Main thread.
|
||||
std::string KeyLabel(double stored)
|
||||
{
|
||||
if (stored == double(settings::keynames::kNone))
|
||||
return kNoKey;
|
||||
if (!(stored >= 1 && stored < double(SDL_SCANCODE_COUNT)))
|
||||
return "?";
|
||||
SDL_Scancode sc = SDL_Scancode(int(stored));
|
||||
const char* name = SDL_GetKeyName(SDL_GetKeyFromScancode(sc, SDL_KMOD_NONE, false));
|
||||
if (!name || !name[0])
|
||||
name = SDL_GetScancodeName(sc);
|
||||
if (!name || !name[0])
|
||||
name = settings::keynames::Token(uint16_t(stored));
|
||||
return name && name[0] ? name : "?";
|
||||
}
|
||||
|
||||
// A key the wait refused, for the help line.
|
||||
std::string RefusedName(SDL_Scancode sc)
|
||||
{
|
||||
std::string name = KeyLabel(double(sc));
|
||||
return name == "?" || name == kNoKey ? "That key" : name;
|
||||
}
|
||||
|
||||
// The other controls that have any of this row's keys ("D-pad
|
||||
// Left"), in page order, each once; "" when none.
|
||||
std::string SharedWith(const hid::keymap::Entry& e)
|
||||
{
|
||||
const double mine[2] = { settings::GetStoredValue(e.id), settings::GetStoredValue(e.id2) };
|
||||
std::string text;
|
||||
for (const auto& other : hid::keymap::kEntries)
|
||||
{
|
||||
if (other.control == e.control)
|
||||
continue;
|
||||
const double theirs[2] = { settings::GetStoredValue(other.id), settings::GetStoredValue(other.id2) };
|
||||
bool shared = false;
|
||||
for (double m : mine)
|
||||
for (double th : theirs)
|
||||
shared |= m != double(settings::keynames::kNone) && m == th;
|
||||
if (shared)
|
||||
text += (text.empty() ? "" : ", ") + std::string(settings::Info(other.id).label);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// A key cell is as wide as the widest name among the 48 keys, "Press
|
||||
// a key" or "none"; a row has two and a gap.
|
||||
float KeyCellWidth()
|
||||
{
|
||||
float width = std::max(ImGui::CalcTextSize(kPressAKey).x, ImGui::CalcTextSize(kNoKey).x);
|
||||
for (const auto& o : settings::Options())
|
||||
if (o.type == settings::Type::Key)
|
||||
width = std::max(width, ImGui::CalcTextSize(KeyLabel(settings::GetStoredValue(o.id)).c_str()).x);
|
||||
return width;
|
||||
}
|
||||
|
||||
float KeyCellGap()
|
||||
{
|
||||
return kSpaceM * Scale();
|
||||
}
|
||||
|
||||
// Widest value this option can show, for the value column.
|
||||
float ValueWidth(const settings::Option& o)
|
||||
{
|
||||
@@ -77,6 +170,9 @@ namespace ui
|
||||
for (const auto& e : o.entries)
|
||||
measure(e.value);
|
||||
break;
|
||||
case settings::Type::Key:
|
||||
width = 2 * KeyCellWidth() + KeyCellGap();
|
||||
break;
|
||||
default:
|
||||
measure(o.min);
|
||||
measure(o.max);
|
||||
@@ -250,6 +346,110 @@ namespace ui
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// A Controls row: the two keys that drive the control, as two cells.
|
||||
// Left and Right choose a cell (the focused row's choice; another
|
||||
// row starts on its first); A or Enter waits for a key for it, which
|
||||
// SettingsMenuTakesKey takes from the event loop before ImGui sees
|
||||
// it; Y or Backspace empties it; X or Delete puts both keys back to
|
||||
// their defaults. A click or tap only selects the row, and the cell
|
||||
// under the pointer, as on the other pages. The wait follows the
|
||||
// focus (the D-pad moving on ends it) and B ends it (below, in
|
||||
// DrawSettings, where B would otherwise close the menu).
|
||||
void KeyRow(const hid::keymap::Entry& e, float valueWidth, bool focus)
|
||||
{
|
||||
const float s = Scale();
|
||||
const float rowHeight = RowHeight();
|
||||
const settings::Option& o = settings::Info(e.id);
|
||||
const Id ids[2] = { e.id, e.id2 };
|
||||
ImGui::PushID(int(e.id));
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowHeight);
|
||||
ImGui::TableNextColumn();
|
||||
if (focus)
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + LabelInset());
|
||||
ImVec2 labelPos = ImGui::GetCursorScreenPos();
|
||||
bool activated = ImGui::Selectable("##row", false,
|
||||
ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap, ImVec2(0, rowHeight));
|
||||
bool focused = ImGui::IsItemFocused();
|
||||
if (focused || (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByItem) && s_described == Id::Count))
|
||||
s_described = e.id;
|
||||
const float mid = RowLabel(labelPos, o.label);
|
||||
|
||||
if (focused && s_slotRow != e.id)
|
||||
{
|
||||
s_slotRow = e.id;
|
||||
s_slot = 0;
|
||||
}
|
||||
const bool waiting = s_waiting == e.id || s_waiting == e.id2;
|
||||
if (waiting && !focused)
|
||||
EndWait();
|
||||
|
||||
// The cells, where other pages put their value (the arrows'
|
||||
// room kept so the columns line up from page to page).
|
||||
ImGui::TableNextColumn();
|
||||
const float arrow = ImGui::GetFrameHeight();
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (rowHeight - arrow) * 0.5f);
|
||||
const ImVec2 min = ImGui::GetCursorScreenPos();
|
||||
ImGui::Dummy(ImVec2(valueWidth + arrow * 2, arrow));
|
||||
const float gap = KeyCellGap(), cell = (valueWidth - gap) * 0.5f;
|
||||
const float cellX[2] = { min.x + arrow, min.x + arrow + cell + gap };
|
||||
|
||||
const bool byPointer = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||
if (activated && byPointer)
|
||||
{
|
||||
const float x = ImGui::GetMousePos().x;
|
||||
for (int i = 0; i < 2; i++)
|
||||
if (x >= cellX[i] && x < cellX[i] + cell)
|
||||
s_slot = i;
|
||||
}
|
||||
else if (activated)
|
||||
{
|
||||
s_waiting = ids[s_slot];
|
||||
s_refused = SDL_SCANCODE_UNKNOWN;
|
||||
}
|
||||
else if (focused && s_waiting == Id::Count)
|
||||
{
|
||||
if (Pressed(ImGuiKey_GamepadDpadLeft) || Pressed(ImGuiKey_GamepadLStickLeft) || Pressed(ImGuiKey_LeftArrow))
|
||||
s_slot = 0;
|
||||
if (Pressed(ImGuiKey_GamepadDpadRight) || Pressed(ImGuiKey_GamepadLStickRight) || Pressed(ImGuiKey_RightArrow))
|
||||
s_slot = 1;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_GamepadFaceUp, false) || ImGui::IsKeyPressed(ImGuiKey_Backspace, false))
|
||||
{
|
||||
const settings::Option& slot = settings::Info(ids[s_slot]);
|
||||
LogChange(slot, settings::GetStoredValue(slot.id), settings::Set(slot.id, settings::keynames::kNone));
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_GamepadFaceLeft, false) || ImGui::IsKeyPressed(ImGuiKey_Delete, false))
|
||||
for (Id id : ids)
|
||||
LogChange(settings::Info(id), settings::GetStoredValue(id), settings::Reset(id));
|
||||
}
|
||||
|
||||
ImDrawList* draw = ImGui::GetWindowDrawList();
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
const bool waitingHere = s_waiting == ids[i];
|
||||
const bool chosen = focused && s_slot == i;
|
||||
const double stored = settings::GetStoredValue(ids[i]);
|
||||
const std::string text = waitingHere ? kPressAKey : KeyLabel(stored);
|
||||
const ImVec2 size = ImGui::CalcTextSize(text.c_str());
|
||||
ImU32 color;
|
||||
if (waitingHere)
|
||||
{
|
||||
PushAccentText();
|
||||
color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
PopAccentText();
|
||||
}
|
||||
else if (chosen)
|
||||
color = ImGui::GetColorU32(ImGuiCol_PlotLines);
|
||||
else
|
||||
color = ImGui::GetColorU32(stored == double(settings::keynames::kNone) ? ImGuiCol_TextDisabled : ImGuiCol_Text);
|
||||
const ImVec2 at(cellX[i] + (cell - size.x) * 0.5f, CentredTextTop(mid));
|
||||
draw->AddText(at, color, text.c_str());
|
||||
if (chosen) // underlined too, so an empty cell shows which it is
|
||||
draw->AddLine(ImVec2(at.x, at.y + size.y + 2.0f * s), ImVec2(at.x + size.x, at.y + size.y + 2.0f * s), color, 1.0f * s);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
const char* BugReportValue()
|
||||
{
|
||||
switch (report::GetBugReportState())
|
||||
@@ -427,7 +627,30 @@ namespace ui
|
||||
return;
|
||||
}
|
||||
const settings::Option& o = settings::Info(s_described);
|
||||
ImGui::TextWrapped("%s", o.description);
|
||||
if (o.type == settings::Type::Key)
|
||||
{
|
||||
const hid::keymap::Entry& e = hid::keymap::EntryOf(o.id);
|
||||
if (s_waiting == e.id || s_waiting == e.id2)
|
||||
{
|
||||
PushAccentText();
|
||||
if (s_refused != SDL_SCANCODE_UNKNOWN)
|
||||
ImGui::Text("%s can't be used here. Press another key; Esc or B cancels.", RefusedName(s_refused).c_str());
|
||||
else
|
||||
ImGui::Text("Press the key for %s. Esc or B cancels.", o.label);
|
||||
PopAccentText();
|
||||
ImGui::TextDisabled(" ");
|
||||
return;
|
||||
}
|
||||
ImGui::TextWrapped("%s", o.description);
|
||||
// The keyboard's keys for what the legend names in
|
||||
// controller terms (Y, X); and a key on several controls is
|
||||
// fine (the defaults share the arrows), but say which, in
|
||||
// case it wasn't meant.
|
||||
std::string also = SharedWith(e);
|
||||
ImGui::TextDisabled("Enter sets the key, Backspace clears it, Delete restores the row.%s%s%s",
|
||||
also.empty() ? "" : " Also: ", also.c_str(), also.empty() ? "" : ".");
|
||||
return;
|
||||
}
|
||||
if (o.id == Id::Fullscreen && video::FullscreenLocked())
|
||||
{
|
||||
ImGui::TextDisabled("Always on in Game Mode: Steam sets the resolution (Properties > Game Resolution).");
|
||||
@@ -500,18 +723,43 @@ namespace ui
|
||||
const bool dialog = ImGui::IsPopupOpen("reset") || UpdateDialogOpen() || QuitPromptOpen();
|
||||
if (int delta = dialog ? 0 : PageDelta())
|
||||
{
|
||||
s_category = Category((int(s_category) + delta + kCategories) % kCategories);
|
||||
Category next = s_category;
|
||||
do
|
||||
next = Category((int(next) + delta + kCategories) % kCategories);
|
||||
while (!PageShown(next));
|
||||
s_category = next;
|
||||
s_tabPending = true;
|
||||
s_focusFirstRow = true;
|
||||
}
|
||||
if (!PageShown(s_category)) // the keyboard went away while on Controls
|
||||
{
|
||||
s_category = Category::Input;
|
||||
s_tabPending = true;
|
||||
s_focusFirstRow = true;
|
||||
}
|
||||
// X on the Controls page is the rows' (KeyRow: the row's default
|
||||
// keys). To ImGui it is also the window-switching key: a press
|
||||
// starts the toggle and the release moves the cursor to the root
|
||||
// window. Owning the key while the page shows keeps that off; the
|
||||
// rows' IsKeyPressed, which accepts any owner, still sees it.
|
||||
if (s_category == Category::Controls)
|
||||
ImGui::SetKeyOwner(ImGuiKey_GamepadFaceLeft, ImHashStr("controls-x"));
|
||||
s_described = Id::Count;
|
||||
s_describedBugReport = false;
|
||||
s_describedUpdate = false;
|
||||
if (s_waiting != Id::Count && settings::Info(s_waiting).category != s_category)
|
||||
EndWait();
|
||||
// For the B check at the end: B's nav cancel (in ImGui's NewFrame,
|
||||
// before this) takes the waiting row's focus, and KeyRow then ends
|
||||
// the wait before that check could see it.
|
||||
const bool wasWaiting = s_waiting != Id::Count;
|
||||
if (ImGui::BeginTabBar("pages", ImGuiTabBarFlags_NoTooltip))
|
||||
{
|
||||
for (int c = 0; c < kCategories; c++)
|
||||
{
|
||||
Category category = Category(c);
|
||||
if (!PageShown(category))
|
||||
continue;
|
||||
ImGuiTabItemFlags tabFlags = (s_tabPending && category == s_category) ? ImGuiTabItemFlags_SetSelected : 0;
|
||||
ImGui::PushItemFlag(ImGuiItemFlags_NoNav, true);
|
||||
bool open = ImGui::BeginTabItem(settings::CategoryName(category), nullptr, tabFlags);
|
||||
@@ -547,9 +795,12 @@ namespace ui
|
||||
if (rows)
|
||||
{
|
||||
float valueWidth = 0.0f;
|
||||
for (const auto& o : settings::Options())
|
||||
if (o.category == category && !o.hidden)
|
||||
valueWidth = std::max(valueWidth, ValueWidth(o));
|
||||
if (category == Category::Controls)
|
||||
valueWidth = 2 * KeyCellWidth() + KeyCellGap();
|
||||
else
|
||||
for (const auto& o : settings::Options())
|
||||
if (o.category == category && !o.hidden)
|
||||
valueWidth = std::max(valueWidth, ValueWidth(o));
|
||||
if (category == Category::Advanced)
|
||||
for (const char* text : { "Saving...", "Checking...", UpdateRowValue() })
|
||||
valueWidth = std::max(valueWidth, ImGui::CalcTextSize(text).x);
|
||||
@@ -560,12 +811,19 @@ namespace ui
|
||||
{
|
||||
ImGui::TableSetupColumn("label", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("value", ImGuiTableColumnFlags_WidthFixed, valueWidth + arrow * 2 + ValueInset());
|
||||
for (const auto& o : settings::Options())
|
||||
if (o.category == category && !o.hidden)
|
||||
if (category == Category::Controls)
|
||||
for (const auto& e : hid::keymap::kEntries)
|
||||
{
|
||||
OptionRow(o, valueWidth, focusFirst);
|
||||
KeyRow(e, valueWidth, focusFirst);
|
||||
focusFirst = false;
|
||||
}
|
||||
else
|
||||
for (const auto& o : settings::Options())
|
||||
if (o.category == category && !o.hidden)
|
||||
{
|
||||
OptionRow(o, valueWidth, focusFirst);
|
||||
focusFirst = false;
|
||||
}
|
||||
if (category == Category::Advanced)
|
||||
{
|
||||
BugReportRow(valueWidth);
|
||||
@@ -627,7 +885,10 @@ namespace ui
|
||||
#endif
|
||||
ImGui::SameLine();
|
||||
{
|
||||
const char* legend = "D-pad: choose, change LB RB: page B: close";
|
||||
// (The Controls page leaves out "D-pad: choose": with Quit Game in the
|
||||
// footer its legend was wider than the window and lost "B: close".)
|
||||
const char* legend = s_category == Category::Controls ? "A: set key Y: clear X: default LB RB: page B: close"
|
||||
: "D-pad: choose, change LB RB: page B: close";
|
||||
float w = ImGui::CalcTextSize(legend).x;
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowContentRegionMax().x - w));
|
||||
ImGui::TextDisabled("%s", legend);
|
||||
@@ -661,15 +922,29 @@ namespace ui
|
||||
if (DrawQuitPrompt())
|
||||
popup = true;
|
||||
#endif
|
||||
// B (or Esc) closes the menu; while a Controls row was waiting for
|
||||
// a key at the top of this frame it ends the wait instead (the
|
||||
// cursor goes with it, as after the Reset dialog: the next D-pad
|
||||
// press brings it back). The Esc key itself never gets here then:
|
||||
// SettingsMenuTakesKey took its press.
|
||||
if (!popup && ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && !ImGui::IsAnyItemActive() &&
|
||||
(ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false) || ImGui::IsKeyPressed(ImGuiKey_Escape, false)))
|
||||
CloseSettings();
|
||||
{
|
||||
if (wasWaiting)
|
||||
EndWait();
|
||||
else
|
||||
CloseSettings();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ResetSettingsMenu()
|
||||
{
|
||||
s_opened = true;
|
||||
EndWait(); // a wait the menu was closed under is over
|
||||
s_takenKey = SDL_SCANCODE_UNKNOWN;
|
||||
s_slotRow = Id::Count;
|
||||
s_slot = 0;
|
||||
// A dialog the menu was closed under (Start or Back alone closes it
|
||||
// from anywhere) doesn't come back with it.
|
||||
if (ImGuiContext* g = ImGui::GetCurrentContext(); g && g->OpenPopupStack.Size > 0)
|
||||
@@ -677,6 +952,48 @@ namespace ui
|
||||
ForgetUpdateDialog();
|
||||
}
|
||||
|
||||
bool SettingsMenuTakesKey(const SDL_KeyboardEvent& key)
|
||||
{
|
||||
// The key a wait took or refused stays down a moment longer. ImGui never saw
|
||||
// it go down, so its OS repeats would read as fresh presses (Enter
|
||||
// would start a new wait, an arrow would move the cursor): they and
|
||||
// its release stay out of ImGui too.
|
||||
if (s_takenKey != SDL_SCANCODE_UNKNOWN && key.scancode == s_takenKey)
|
||||
{
|
||||
if (!key.down)
|
||||
s_takenKey = SDL_SCANCODE_UNKNOWN;
|
||||
if (!key.down || key.repeat)
|
||||
return true;
|
||||
s_takenKey = SDL_SCANCODE_UNKNOWN; // pressed anew: its release went by unseen
|
||||
}
|
||||
if (s_waiting == Id::Count || !key.down || key.repeat)
|
||||
return false;
|
||||
const settings::Option& o = settings::Info(s_waiting);
|
||||
s_takenKey = key.scancode;
|
||||
if (key.scancode == SDL_SCANCODE_ESCAPE)
|
||||
{
|
||||
fprintf(stderr, "[settings] menu: %s.%s: no new key (Esc)\n", o.table, o.key);
|
||||
EndWait();
|
||||
return true;
|
||||
}
|
||||
// Not a key of the table (Cmd / Windows, F1, the media keys), or a
|
||||
// key pressed with Cmd / Windows held: the wait goes on and the help
|
||||
// line says so. (Cmd+, still closes the menu on a Mac: ProcessEvent's
|
||||
// toggle sees the comma before this does; the Cmd press before it
|
||||
// lands here and is refused.) Its repeats and release stay out of
|
||||
// ImGui like a taken key's: s_takenKey is already set.
|
||||
if (key.scancode == SDL_SCANCODE_UNKNOWN || !settings::keynames::Known(uint16_t(key.scancode)) || (key.mod & SDL_KMOD_GUI))
|
||||
{
|
||||
s_refused = key.scancode;
|
||||
fprintf(stderr, "[settings] menu: %s.%s: %s can't be used\n", o.table, o.key, RefusedName(key.scancode).c_str());
|
||||
return true;
|
||||
}
|
||||
double from = settings::GetStoredValue(o.id);
|
||||
LogChange(o, from, settings::Set(o.id, double(key.scancode)));
|
||||
EndWait();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ForgetQuitPrompt()
|
||||
{
|
||||
#if SB_QUIT_GAME
|
||||
|
||||
+25
-2
@@ -638,8 +638,14 @@ namespace ui
|
||||
s_settingsOpen ? CloseSettings() : OpenSettings();
|
||||
return;
|
||||
}
|
||||
if (s_settingsOpen && SettingsMenuTakesKey(e.key))
|
||||
return; // a Controls row took it: ImGui doesn't act on it too
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_KEY_UP:
|
||||
if (s_settingsOpen && SettingsMenuTakesKey(e.key))
|
||||
return; // the release of a key a Controls row took
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||
{
|
||||
@@ -723,6 +729,10 @@ namespace ui
|
||||
|
||||
// NFSMW_UI_KEYS="3:F1,4:Down,4.5:Right": key presses (SDL key names)
|
||||
// at seconds since launch, for testing the menus without a person.
|
||||
// A key is pressed and released at once; "Hold Return" only presses
|
||||
// it, "Repeat Return" is the OS's auto-repeat of a held key (a
|
||||
// key-down marked repeat) and "Release Return" lets it go, for a key
|
||||
// held across other keys.
|
||||
// FocusLost and FocusGained send the window those events instead (as
|
||||
// switching to another window would: Mute in Background). Shot saves
|
||||
// the next presented frame as build/ui_shot_<n>.ppm: on the keys'
|
||||
@@ -735,7 +745,8 @@ namespace ui
|
||||
// motion a mouse moving there makes is posted.
|
||||
constexpr SDL_Keycode kFocusLost = SDLK_EXTENDED_MASK | 0x7F01, kFocusGained = SDLK_EXTENDED_MASK | 0x7F02,
|
||||
kShot = SDLK_EXTENDED_MASK | 0x7F03, kMouseMove = SDLK_EXTENDED_MASK | 0x7F04;
|
||||
struct Key { double time; SDL_Keycode key; };
|
||||
enum class Press : uint8_t { Tap, Hold, Repeat, Release };
|
||||
struct Key { double time; SDL_Keycode key; Press press = Press::Tap; };
|
||||
static const auto start = std::chrono::steady_clock::now();
|
||||
static std::vector<Key> keys = [] {
|
||||
std::vector<Key> parsed;
|
||||
@@ -749,12 +760,20 @@ namespace ui
|
||||
if (colon == std::string::npos)
|
||||
continue;
|
||||
std::string name = item.substr(colon + 1);
|
||||
Press press = Press::Tap;
|
||||
for (auto [prefix, kind] : { std::pair{ "Hold ", Press::Hold }, std::pair{ "Repeat ", Press::Repeat },
|
||||
std::pair{ "Release ", Press::Release } })
|
||||
if (name.starts_with(prefix))
|
||||
{
|
||||
press = kind;
|
||||
name = name.substr(strlen(prefix));
|
||||
}
|
||||
SDL_Keycode key = name == "FocusLost" ? kFocusLost : name == "FocusGained" ? kFocusGained
|
||||
: name == "Shot" ? kShot
|
||||
: name == "MouseMove" ? kMouseMove
|
||||
: SDL_GetKeyFromName(name.c_str());
|
||||
if (key != SDLK_UNKNOWN)
|
||||
parsed.push_back({ std::atof(item.c_str()), key });
|
||||
parsed.push_back({ std::atof(item.c_str()), key, press });
|
||||
else
|
||||
fprintf(stderr, "[ui] NFSMW_UI_KEYS: unknown key %s\n", item.c_str() + colon + 1);
|
||||
}
|
||||
@@ -821,8 +840,11 @@ namespace ui
|
||||
fprintf(stderr, "[ui] NFSMW_UI_KEYS: window focus %s\n", keys[next].key == kFocusLost ? "lost" : "gained");
|
||||
continue;
|
||||
}
|
||||
const Press press = keys[next].press;
|
||||
for (bool down : { true, false })
|
||||
{
|
||||
if ((down && press == Press::Release) || (!down && (press == Press::Hold || press == Press::Repeat)))
|
||||
continue;
|
||||
SDL_Event e{};
|
||||
e.type = down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP;
|
||||
e.key.timestamp = SDL_GetTicksNS();
|
||||
@@ -830,6 +852,7 @@ namespace ui
|
||||
e.key.key = keys[next].key;
|
||||
e.key.scancode = SDL_GetScancodeFromKey(keys[next].key, nullptr);
|
||||
e.key.down = down;
|
||||
e.key.repeat = press == Press::Repeat;
|
||||
SDL_PushEvent(&e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,12 @@ namespace ui
|
||||
// Screens (settings_menu.cpp, perf_overlay.cpp, installer_screen.cpp).
|
||||
void DrawSettings();
|
||||
void ResetSettingsMenu(); // on opening: focus the first row, no dialog left open
|
||||
// Main thread, from ProcessEvent on a key-down or key-up while the menu
|
||||
// is open, after the F1 toggle: true when a Controls row was waiting
|
||||
// for a key and took this one, refused it (the wait goes on), or Esc
|
||||
// cancelled it, or this is a repeat or the release of a key it took or
|
||||
// refused, so ImGui must not see it.
|
||||
bool SettingsMenuTakesKey(const SDL_KeyboardEvent& key);
|
||||
void DrawPerfOverlay();
|
||||
|
||||
// Settings > Advanced > Check for Updates (update_dialog.cpp, the check
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// The keys a Controls row can hold (Settings > Controls), named for
|
||||
// settings.toml: "w", "left_shift", "keypad_1", or "none" for no key. The
|
||||
// code is the key's USB HID usage, which is what SDL's scancodes are (A is
|
||||
// 4), so a binding is the physical key whatever the layout; SDL itself
|
||||
// stays out of this header and of settings.cpp, and tests/key_names_test.cpp
|
||||
// checks every row against SDL's name for the code (hid/input.cpp pins a
|
||||
// few at compile time). SDL's own names won't do for the file: some change
|
||||
// once video starts (on a Mac Left Alt becomes "Left Option"), while the
|
||||
// settings load before that, and some keys share one. The table is also
|
||||
// the list of what can be bound: F1 (it opens the menu), Cmd / Windows and
|
||||
// the media keys are left out on purpose. Header only.
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <string_view>
|
||||
|
||||
namespace settings::keynames
|
||||
{
|
||||
constexpr uint16_t kNone = 0; // no key; "none" in the file
|
||||
constexpr uint16_t kUnknown = 0xFFFF; // Code(): not a name in the table
|
||||
|
||||
struct Key
|
||||
{
|
||||
uint16_t code;
|
||||
const char* token;
|
||||
};
|
||||
|
||||
// Ascending by code. The tokens are SDL's scancode constants in lower
|
||||
// case with underscores: SDL_SCANCODE_LEFTBRACKET is "left_bracket",
|
||||
// SDL_SCANCODE_KP_1 "keypad_1", SDL_SCANCODE_NUMLOCKCLEAR "num_lock".
|
||||
constexpr Key kKeys[] = {
|
||||
{ 4, "a" }, { 5, "b" }, { 6, "c" }, { 7, "d" }, { 8, "e" }, { 9, "f" }, { 10, "g" }, { 11, "h" }, { 12, "i" },
|
||||
{ 13, "j" }, { 14, "k" }, { 15, "l" }, { 16, "m" }, { 17, "n" }, { 18, "o" }, { 19, "p" }, { 20, "q" }, { 21, "r" },
|
||||
{ 22, "s" }, { 23, "t" }, { 24, "u" }, { 25, "v" }, { 26, "w" }, { 27, "x" }, { 28, "y" }, { 29, "z" },
|
||||
{ 30, "1" }, { 31, "2" }, { 32, "3" }, { 33, "4" }, { 34, "5" }, { 35, "6" }, { 36, "7" }, { 37, "8" }, { 38, "9" }, { 39, "0" },
|
||||
{ 40, "return" }, { 41, "escape" }, { 42, "backspace" }, { 43, "tab" }, { 44, "space" },
|
||||
{ 45, "minus" }, { 46, "equals" }, { 47, "left_bracket" }, { 48, "right_bracket" }, { 49, "backslash" },
|
||||
{ 50, "non_us_hash" }, { 51, "semicolon" }, { 52, "apostrophe" }, { 53, "grave" }, { 54, "comma" },
|
||||
{ 55, "period" }, { 56, "slash" }, { 57, "caps_lock" },
|
||||
{ 59, "f2" }, { 60, "f3" }, { 61, "f4" }, { 62, "f5" }, { 63, "f6" }, { 64, "f7" }, { 65, "f8" }, { 66, "f9" },
|
||||
{ 67, "f10" }, { 68, "f11" }, { 69, "f12" },
|
||||
{ 70, "print_screen" }, { 71, "scroll_lock" }, { 72, "pause" }, { 73, "insert" }, { 74, "home" },
|
||||
{ 75, "page_up" }, { 76, "delete" }, { 77, "end" }, { 78, "page_down" },
|
||||
{ 79, "right" }, { 80, "left" }, { 81, "down" }, { 82, "up" },
|
||||
{ 83, "num_lock" }, { 84, "keypad_divide" }, { 85, "keypad_multiply" }, { 86, "keypad_minus" },
|
||||
{ 87, "keypad_plus" }, { 88, "keypad_enter" },
|
||||
{ 89, "keypad_1" }, { 90, "keypad_2" }, { 91, "keypad_3" }, { 92, "keypad_4" }, { 93, "keypad_5" },
|
||||
{ 94, "keypad_6" }, { 95, "keypad_7" }, { 96, "keypad_8" }, { 97, "keypad_9" }, { 98, "keypad_0" },
|
||||
{ 99, "keypad_period" }, { 100, "non_us_backslash" }, { 101, "application" }, { 103, "keypad_equals" },
|
||||
{ 104, "f13" }, { 105, "f14" }, { 106, "f15" }, { 107, "f16" }, { 108, "f17" }, { 109, "f18" },
|
||||
{ 110, "f19" }, { 111, "f20" }, { 112, "f21" }, { 113, "f22" }, { 114, "f23" }, { 115, "f24" },
|
||||
{ 133, "keypad_comma" },
|
||||
{ 135, "international_1" }, { 136, "international_2" }, { 137, "international_3" },
|
||||
{ 138, "international_4" }, { 139, "international_5" }, { 140, "international_6" },
|
||||
{ 224, "left_ctrl" }, { 225, "left_shift" }, { 226, "left_alt" },
|
||||
{ 228, "right_ctrl" }, { 229, "right_shift" }, { 230, "right_alt" },
|
||||
};
|
||||
constexpr size_t kKeyCount = std::size(kKeys);
|
||||
|
||||
namespace detail
|
||||
{
|
||||
constexpr char Lower(char c)
|
||||
{
|
||||
return c >= 'A' && c <= 'Z' ? char(c - 'A' + 'a') : c;
|
||||
}
|
||||
|
||||
// `text` in any case against a lower-case token.
|
||||
constexpr bool Same(std::string_view text, const char* token)
|
||||
{
|
||||
size_t i = 0;
|
||||
for (; i < text.size() && token[i]; i++)
|
||||
if (Lower(text[i]) != token[i])
|
||||
return false;
|
||||
return i == text.size() && token[i] == '\0';
|
||||
}
|
||||
}
|
||||
|
||||
// The file's name for a code: "none" for kNone; nullptr for a code that
|
||||
// isn't a key here.
|
||||
constexpr const char* Token(uint16_t code)
|
||||
{
|
||||
if (code == kNone)
|
||||
return "none";
|
||||
for (const Key& k : kKeys)
|
||||
if (k.code == code)
|
||||
return k.token;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The code for a name in the file, in any case; kUnknown when it isn't one.
|
||||
constexpr uint16_t Code(std::string_view token)
|
||||
{
|
||||
if (detail::Same(token, "none"))
|
||||
return kNone;
|
||||
for (const Key& k : kKeys)
|
||||
if (detail::Same(token, k.token))
|
||||
return k.code;
|
||||
return kUnknown;
|
||||
}
|
||||
|
||||
// A value a Key option can hold: no key, or a key of the table.
|
||||
constexpr bool Known(uint16_t code)
|
||||
{
|
||||
return code == kNone || Token(code) != nullptr;
|
||||
}
|
||||
}
|
||||
+136
-7
@@ -27,6 +27,8 @@
|
||||
#include <TargetConditionals.h> // TARGET_OS_IOS: __APPLE__ alone means macOS or iOS
|
||||
#endif
|
||||
|
||||
#include "key_names.h" // the Key options: the keys and their names in the file
|
||||
|
||||
// toml++ (header-only), as vendored by XenonRecomp. The fallback path keeps
|
||||
// this file building before runtime/CMakeLists.txt adds the include
|
||||
// directory (the runtime globs every .cpp under runtime/).
|
||||
@@ -45,9 +47,23 @@ namespace settings
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// FSR 1 isn't offered on iPhone and iPad: its EASU pass costs an
|
||||
// Apple GPU 2-3x the bicubic pass, and a phone's 1x picture is
|
||||
// enlarged about 2x every frame. Without the entry, "fsr1" in
|
||||
// settings.toml or NFSMW_SCALING reads as unknown (bicubic), so the
|
||||
// presenter never makes an FSR 1 shader or pipeline there (it makes
|
||||
// them only for a frame whose pass is FSR 1: video/upscale.h).
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS
|
||||
constexpr bool kFsr1Offered = false;
|
||||
#else
|
||||
constexpr bool kFsr1Offered = true;
|
||||
#endif
|
||||
constexpr EnumEntry kScalingEntries[] = {
|
||||
{ int32_t(ScalingMode::Bicubic), "bicubic", "Bicubic" },
|
||||
{ int32_t(ScalingMode::Linear), "linear", "Bilinear" },
|
||||
#if !(defined(__APPLE__) && TARGET_OS_IOS)
|
||||
{ int32_t(ScalingMode::Fsr1), "fsr1", "FSR 1" },
|
||||
#endif
|
||||
};
|
||||
|
||||
// Frame Rate: the game's frames per second (60, or 30 on a GPU that
|
||||
@@ -104,6 +120,31 @@ namespace settings
|
||||
{ kAnisotropyGame, "game", "Original" },
|
||||
};
|
||||
|
||||
// A Controls row's key: the key (user/key_names.h, by its name
|
||||
// there) that drives one Xbox 360 control. Two per control, the
|
||||
// second named <key>_2 with the same label. No environment
|
||||
// variable: 48 of them would clutter the file's comments for nobody.
|
||||
constexpr Option KeyOption(Id id, const char* key, const char* label, const char* description, const char* token)
|
||||
{
|
||||
return { .id = id, .category = Category::Controls, .type = Type::Key, .apply = Apply::Live,
|
||||
.table = "keys", .key = key, .label = label, .description = description, .env = nullptr,
|
||||
.defaultValue = double(keynames::Code(token)) };
|
||||
}
|
||||
|
||||
constexpr Option KeyOption2(Id id, const char* key, const char* label, const char* token)
|
||||
{
|
||||
return KeyOption(id, key, label, "A second key for the same control, or none.", token);
|
||||
}
|
||||
|
||||
// A stored or offered value as a key code: kUnknown unless it's a
|
||||
// whole number the key table knows (none included).
|
||||
constexpr uint16_t KeyCode(double v)
|
||||
{
|
||||
if (!(v >= 0 && v < double(keynames::kUnknown)) || v != uint16_t(v))
|
||||
return keynames::kUnknown;
|
||||
return keynames::Known(uint16_t(v)) ? uint16_t(v) : keynames::kUnknown;
|
||||
}
|
||||
|
||||
// Defaults are what the runtime did before settings existed, so an
|
||||
// empty settings.toml changes nothing.
|
||||
constexpr Option kOptions[] = {
|
||||
@@ -130,7 +171,9 @@ namespace settings
|
||||
.env = "NFSMW_AA", .defaultValue = 1 },
|
||||
{ .id = Id::Scaling, .category = Category::Graphics, .type = Type::Enum, .apply = Apply::Live,
|
||||
.table = "video", .key = "scaling", .label = "Upscaling",
|
||||
.description = "How the 720p image is scaled to the window. Bicubic is sharper, bilinear softer.",
|
||||
.description = kFsr1Offered
|
||||
? "How the game's picture is enlarged to the window. Bicubic is sharp, bilinear softer. FSR 1 (AMD FidelityFX Super Resolution 1) keeps curves, wires and diagonals smooth when the window is much larger than the Internal Resolution (1x on a 1440p or 4K screen), for a little more GPU time."
|
||||
: "How the game's picture is enlarged to the screen. Bicubic is sharp, bilinear softer.",
|
||||
.env = "NFSMW_SCALING", .defaultValue = double(ScalingMode::Bicubic), .entries = kScalingEntries },
|
||||
{ .id = Id::AspectRatio, .category = Category::Display, .type = Type::Enum, .apply = Apply::Live,
|
||||
.table = "video", .key = "aspect_ratio", .label = "Aspect Ratio",
|
||||
@@ -147,7 +190,9 @@ namespace settings
|
||||
.env = "NFSMW_DISPLAY_GAMMA", .defaultValue = 1 },
|
||||
{ .id = Id::Sharpening, .category = Category::Graphics, .type = Type::Float, .apply = Apply::Live,
|
||||
.table = "video", .key = "sharpening", .label = "Sharpening",
|
||||
.description = "Extra sharpness for bicubic upscaling. 0 turns it off.",
|
||||
.description = kFsr1Offered
|
||||
? "Extra sharpness for bicubic and FSR 1 upscaling (for FSR 1, the share of its sharpening pass's full strength). 0 turns it off."
|
||||
: "Extra sharpness for bicubic upscaling. 0 turns it off.",
|
||||
.env = "NFSMW_SHARPEN", .defaultValue = 0.3, .min = 0, .max = 1, .step = 0.05 },
|
||||
{ .id = Id::Anisotropy, .category = Category::Graphics, .type = Type::Enum, .apply = Apply::Live,
|
||||
.table = "video", .key = "anisotropic_filtering", .label = "Anisotropic Filtering",
|
||||
@@ -195,6 +240,57 @@ namespace settings
|
||||
.table = "input", .key = "vibration", .label = "Vibration",
|
||||
.description = "Controller rumble.",
|
||||
.env = "NFSMW_VIBRATION", .defaultValue = 1 },
|
||||
// Controls (Settings > Controls; hid/key_map.h reads them). The
|
||||
// defaults are v0.1.4's keys: the arrows steer as well as A
|
||||
// and D, and are the D-pad; O/L/K/; are the right stick.
|
||||
KeyOption(Id::KeyRightTrigger, "right_trigger", "Right Trigger", "Gas in the game's default layout.", "w"),
|
||||
KeyOption2(Id::KeyRightTrigger2, "right_trigger_2", "Right Trigger", "none"),
|
||||
KeyOption(Id::KeyLeftTrigger, "left_trigger", "Left Trigger", "Brake and reverse in the game's default layout.", "s"),
|
||||
KeyOption2(Id::KeyLeftTrigger2, "left_trigger_2", "Left Trigger", "none"),
|
||||
KeyOption(Id::KeyLeftStickLeft, "left_stick_left", "Left Stick Left", "Steer left.", "a"),
|
||||
KeyOption2(Id::KeyLeftStickLeft2, "left_stick_left_2", "Left Stick Left", "left"),
|
||||
KeyOption(Id::KeyLeftStickRight, "left_stick_right", "Left Stick Right", "Steer right.", "d"),
|
||||
KeyOption2(Id::KeyLeftStickRight2, "left_stick_right_2", "Left Stick Right", "right"),
|
||||
KeyOption(Id::KeyLeftStickUp, "left_stick_up", "Left Stick Up", "The left stick pushed up (menus).", "up"),
|
||||
KeyOption2(Id::KeyLeftStickUp2, "left_stick_up_2", "Left Stick Up", "none"),
|
||||
KeyOption(Id::KeyLeftStickDown, "left_stick_down", "Left Stick Down", "The left stick pulled down.", "down"),
|
||||
KeyOption2(Id::KeyLeftStickDown2, "left_stick_down_2", "Left Stick Down", "none"),
|
||||
KeyOption(Id::KeyA, "a", "A", "The A button.", "space"),
|
||||
KeyOption2(Id::KeyA2, "a_2", "A", "none"),
|
||||
KeyOption(Id::KeyB, "b", "B", "The B button.", "backspace"),
|
||||
KeyOption2(Id::KeyB2, "b_2", "B", "none"),
|
||||
KeyOption(Id::KeyX, "x", "X", "The X button.", "left_shift"),
|
||||
KeyOption2(Id::KeyX2, "x_2", "X", "none"),
|
||||
KeyOption(Id::KeyY, "y", "Y", "The Y button.", "left_ctrl"),
|
||||
KeyOption2(Id::KeyY2, "y_2", "Y", "none"),
|
||||
KeyOption(Id::KeyLB, "lb", "LB", "The left shoulder button.", "q"),
|
||||
KeyOption2(Id::KeyLB2, "lb_2", "LB", "none"),
|
||||
KeyOption(Id::KeyRB, "rb", "RB", "The right shoulder button.", "e"),
|
||||
KeyOption2(Id::KeyRB2, "rb_2", "RB", "none"),
|
||||
KeyOption(Id::KeyDpadUp, "dpad_up", "D-pad Up", "The D-pad, up.", "up"),
|
||||
KeyOption2(Id::KeyDpadUp2, "dpad_up_2", "D-pad Up", "none"),
|
||||
KeyOption(Id::KeyDpadDown, "dpad_down", "D-pad Down", "The D-pad, down.", "down"),
|
||||
KeyOption2(Id::KeyDpadDown2, "dpad_down_2", "D-pad Down", "none"),
|
||||
KeyOption(Id::KeyDpadLeft, "dpad_left", "D-pad Left", "The D-pad, left.", "left"),
|
||||
KeyOption2(Id::KeyDpadLeft2, "dpad_left_2", "D-pad Left", "none"),
|
||||
KeyOption(Id::KeyDpadRight, "dpad_right", "D-pad Right", "The D-pad, right.", "right"),
|
||||
KeyOption2(Id::KeyDpadRight2, "dpad_right_2", "D-pad Right", "none"),
|
||||
KeyOption(Id::KeyStart, "start", "Start", "Start: pause.", "return"),
|
||||
KeyOption2(Id::KeyStart2, "start_2", "Start", "none"),
|
||||
KeyOption(Id::KeyBack, "back", "Back", "Back.", "escape"),
|
||||
KeyOption2(Id::KeyBack2, "back_2", "Back", "none"),
|
||||
KeyOption(Id::KeyRightStickUp, "right_stick_up", "Right Stick Up", "The right stick pushed up.", "o"),
|
||||
KeyOption2(Id::KeyRightStickUp2, "right_stick_up_2", "Right Stick Up", "none"),
|
||||
KeyOption(Id::KeyRightStickDown, "right_stick_down", "Right Stick Down", "The right stick pulled down.", "l"),
|
||||
KeyOption2(Id::KeyRightStickDown2, "right_stick_down_2", "Right Stick Down", "none"),
|
||||
KeyOption(Id::KeyRightStickLeft, "right_stick_left", "Right Stick Left", "The right stick pushed left.", "k"),
|
||||
KeyOption2(Id::KeyRightStickLeft2, "right_stick_left_2", "Right Stick Left", "none"),
|
||||
KeyOption(Id::KeyRightStickRight, "right_stick_right", "Right Stick Right", "The right stick pushed right.", "semicolon"),
|
||||
KeyOption2(Id::KeyRightStickRight2, "right_stick_right_2", "Right Stick Right", "none"),
|
||||
KeyOption(Id::KeyLeftStickClick, "left_stick_click", "Left Stick Click", "Pressing the left stick in.", "z"),
|
||||
KeyOption2(Id::KeyLeftStickClick2, "left_stick_click_2", "Left Stick Click", "none"),
|
||||
KeyOption(Id::KeyRightStickClick, "right_stick_click", "Right Stick Click", "Pressing the right stick in.", "c"),
|
||||
KeyOption2(Id::KeyRightStickClick2, "right_stick_click_2", "Right Stick Click", "none"),
|
||||
// Advanced
|
||||
{ .id = Id::AsyncShaders, .category = Category::Advanced, .type = Type::Bool, .apply = Apply::Live,
|
||||
.table = "advanced", .key = "async_shaders", .label = "Asynchronous Shaders",
|
||||
@@ -232,7 +328,10 @@ namespace settings
|
||||
if (!found)
|
||||
return false;
|
||||
}
|
||||
if (o.env == nullptr || o.label == nullptr || o.description == nullptr)
|
||||
if (o.type == Type::Key && !(KeyCode(o.defaultValue) != keynames::kUnknown && o.min == 0 && o.max == 0 &&
|
||||
o.step == 0 && o.entries.empty() && !o.hidden))
|
||||
return false;
|
||||
if ((o.env == nullptr) != (o.type == Type::Key) || o.label == nullptr || o.description == nullptr)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -429,6 +528,11 @@ namespace settings
|
||||
if (const EnumEntry* e = FindEntry(o, v))
|
||||
return double(e->value);
|
||||
return {};
|
||||
case Type::Key:
|
||||
// No key, or a key of the table: the file holds its name.
|
||||
if (KeyCode(v) == keynames::kUnknown)
|
||||
return {};
|
||||
return v;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -453,6 +557,8 @@ namespace settings
|
||||
text += std::format(", or a number from {} to {}", o.min, o.max);
|
||||
return text;
|
||||
}
|
||||
case Type::Key:
|
||||
return "a key name such as \"w\", \"left_shift\" or \"keypad_1\", or \"none\" (user/key_names.h lists them)";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -476,6 +582,10 @@ namespace settings
|
||||
if (auto n = ParseNumber(text))
|
||||
return EnumFromNumber(o, *n);
|
||||
return {};
|
||||
case Type::Key:
|
||||
if (uint16_t code = keynames::Code(text); code != keynames::kUnknown)
|
||||
return double(code);
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -504,6 +614,8 @@ namespace settings
|
||||
return n;
|
||||
case Type::Enum:
|
||||
return EnumFromNumber(o, *n);
|
||||
case Type::Key:
|
||||
return {}; // names only
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -569,6 +681,8 @@ namespace settings
|
||||
{
|
||||
const Option& o = kOptions[i];
|
||||
s_envSet[i].store(false, std::memory_order_relaxed);
|
||||
if (!o.env)
|
||||
return; // a Key option: the file alone
|
||||
const char* v = std::getenv(o.env);
|
||||
if (!v)
|
||||
return;
|
||||
@@ -718,6 +832,13 @@ namespace settings
|
||||
if (const EnumEntry* e = FindEntry(o, Default(size_t(o.id))))
|
||||
return std::format("\"{}\"", e->token);
|
||||
return std::format("\"{}\"", FindEntry(o, o.defaultValue)->token);
|
||||
case Type::Key:
|
||||
{
|
||||
uint16_t code = KeyCode(v);
|
||||
if (code == keynames::kUnknown)
|
||||
code = KeyCode(o.defaultValue);
|
||||
return std::format("\"{}\"", keynames::Token(code)); // [a-z0-9_]+: nothing to escape
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -727,8 +848,8 @@ namespace settings
|
||||
std::string out =
|
||||
"# SpeedBreaker settings, written by the game's settings menu. Hand edits\n"
|
||||
"# are fine while the game is closed; unknown keys are dropped on the next\n"
|
||||
"# save. The environment variable after each option overrides it for one\n"
|
||||
"# run without changing this file.\n";
|
||||
"# save. The environment variable after an option overrides it for one run\n"
|
||||
"# without changing this file.\n";
|
||||
const char* table = "";
|
||||
for (size_t i = 0; i < kCount; i++)
|
||||
{
|
||||
@@ -742,8 +863,8 @@ namespace settings
|
||||
out += "\n";
|
||||
std::string range = RangeText(o);
|
||||
range[0] = char(std::toupper(uint8_t(range[0])));
|
||||
out += std::format("# {}: {}\n# {}. ({}{})\n", o.label, o.description, range, o.env,
|
||||
o.apply == Apply::Restart ? "; applies at the next launch" : "");
|
||||
std::string override = o.env ? std::format(" ({}{})", o.env, o.apply == Apply::Restart ? "; applies at the next launch" : "") : "";
|
||||
out += std::format("# {}: {}\n# {}.{}\n", o.label, o.description, range, override);
|
||||
out += std::format("{} = {}\n", o.key, TomlValue(o, values[i]));
|
||||
}
|
||||
std::string lastVersion;
|
||||
@@ -932,6 +1053,7 @@ namespace settings
|
||||
case Category::Graphics: return "Graphics";
|
||||
case Category::Audio: return "Audio";
|
||||
case Category::Input: return "Input";
|
||||
case Category::Controls: return "Controls";
|
||||
case Category::Advanced: return "Advanced";
|
||||
default: return "";
|
||||
}
|
||||
@@ -1069,6 +1191,8 @@ namespace settings
|
||||
double index = delta > 0 ? std::floor(position) + delta : delta < 0 ? std::ceil(position) + delta : position;
|
||||
return *Normalize(o, o.min + index * o.step); // not NaN: v and step are finite
|
||||
}
|
||||
case Type::Key:
|
||||
return value; // a key has no neighbour: the menu takes a key press instead
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -1085,6 +1209,11 @@ namespace settings
|
||||
if (const EnumEntry* e = FindEntry(o, value))
|
||||
return e->label;
|
||||
return "?";
|
||||
case Type::Key:
|
||||
{
|
||||
uint16_t code = KeyCode(value);
|
||||
return code == keynames::kUnknown ? "?" : keynames::Token(code);
|
||||
}
|
||||
case Type::Int:
|
||||
case Type::Float:
|
||||
break;
|
||||
|
||||
+39
-5
@@ -14,7 +14,8 @@
|
||||
// that runs before main() calls Load(), so it would miss the file (it
|
||||
// gets the environment and defaults, with a warning in the log).
|
||||
// - An environment variable (NFSMW_AA=0, ...) wins over the file for that
|
||||
// run and is never written back; EnvOverride() tells the menu.
|
||||
// run and is never written back; EnvOverride() tells the menu. Key
|
||||
// options have none.
|
||||
// - A Restart option keeps its launch value for the whole run. Its
|
||||
// consumers read it once (the window's size), so a change mid-run would
|
||||
// reach only part of them. The menu edits the stored value and shows
|
||||
@@ -62,6 +63,34 @@ namespace settings
|
||||
StickDeadzone,
|
||||
TriggerDeadzone,
|
||||
Vibration,
|
||||
// Controls (the [keys] table): the two keyboard keys that drive
|
||||
// each Xbox 360 control, as key codes (user/key_names.h); the
|
||||
// second of a pair is always the first's id + 1 (hid/key_map.h
|
||||
// pairs them). Page order.
|
||||
KeyRightTrigger, KeyRightTrigger2,
|
||||
KeyLeftTrigger, KeyLeftTrigger2,
|
||||
KeyLeftStickLeft, KeyLeftStickLeft2,
|
||||
KeyLeftStickRight, KeyLeftStickRight2,
|
||||
KeyLeftStickUp, KeyLeftStickUp2,
|
||||
KeyLeftStickDown, KeyLeftStickDown2,
|
||||
KeyA, KeyA2,
|
||||
KeyB, KeyB2,
|
||||
KeyX, KeyX2,
|
||||
KeyY, KeyY2,
|
||||
KeyLB, KeyLB2,
|
||||
KeyRB, KeyRB2,
|
||||
KeyDpadUp, KeyDpadUp2,
|
||||
KeyDpadDown, KeyDpadDown2,
|
||||
KeyDpadLeft, KeyDpadLeft2,
|
||||
KeyDpadRight, KeyDpadRight2,
|
||||
KeyStart, KeyStart2,
|
||||
KeyBack, KeyBack2,
|
||||
KeyRightStickUp, KeyRightStickUp2,
|
||||
KeyRightStickDown, KeyRightStickDown2,
|
||||
KeyRightStickLeft, KeyRightStickLeft2,
|
||||
KeyRightStickRight, KeyRightStickRight2,
|
||||
KeyLeftStickClick, KeyLeftStickClick2,
|
||||
KeyRightStickClick, KeyRightStickClick2,
|
||||
// Advanced
|
||||
AsyncShaders,
|
||||
PerformanceOverlay,
|
||||
@@ -69,12 +98,17 @@ namespace settings
|
||||
};
|
||||
constexpr size_t kCount = size_t(Id::Count);
|
||||
|
||||
enum class Type : uint8_t { Bool, Int, Float, Enum };
|
||||
enum class Category : uint8_t { Display, Graphics, Audio, Input, Advanced, Count };
|
||||
// Key: a key code of user/key_names.h (0: no key), saved by its name
|
||||
// there ("w", "left_shift", "none"); the menu takes it from a key press.
|
||||
enum class Type : uint8_t { Bool, Int, Float, Enum, Key };
|
||||
enum class Category : uint8_t { Display, Graphics, Audio, Input, Controls, Advanced, Count };
|
||||
enum class Apply : uint8_t { Live, Restart };
|
||||
|
||||
// Values of the enum options (GetInt returns them).
|
||||
enum class ScalingMode : int32_t { Bicubic = 0, Linear = 1 };
|
||||
// Upscaling: FSR 1 is used only where the picture is enlarged; frames
|
||||
// shown at their size or smaller take the bicubic pass
|
||||
// (video/upscale.h, whose Mode has the same values).
|
||||
enum class ScalingMode : int32_t { Bicubic = 0, Linear = 1, Fsr1 = 2 };
|
||||
// Frame Rate: 60, 30, or Auto (video/frame_rate.h: 60, or 30 while the
|
||||
// device is hot or can't hold 60).
|
||||
constexpr int32_t kFrameRateAuto = 0;
|
||||
@@ -111,7 +145,7 @@ namespace settings
|
||||
const char* key; // key in that table, e.g. "anti_aliasing"
|
||||
const char* label; // menu label
|
||||
const char* description; // one-line menu help
|
||||
const char* env; // the environment variable that overrides it
|
||||
const char* env; // the environment variable that overrides it; nullptr: none (Key options)
|
||||
int envField = 0; // 0: the whole variable; n: its n-th 'x'-separated field (NFSMW_WINDOW=WxH)
|
||||
double defaultValue; // Bool: 0 or 1; Enum: an entry's value
|
||||
// Int and Float: the range and the menu's step. Enum: the range a
|
||||
|
||||
@@ -56,4 +56,29 @@ namespace video::band
|
||||
{
|
||||
return std::clamp(i, s.a, s.b - 1);
|
||||
}
|
||||
|
||||
// FSR 1 (video/fsr1.cpp's GLSL). Its passes work on output pixels: the
|
||||
// picture's pixel i of outSize along an axis has its centre at this
|
||||
// coordinate of the srcSize-texel frame image, and its span is the one
|
||||
// holding it (SpanOf). EASU keeps each of its taps (texels floor(t -
|
||||
// 0.5) - 1 to + 2) in that span with ClampTexel.
|
||||
inline float SourceOf(int32_t i, int32_t outSize, int32_t srcSize)
|
||||
{
|
||||
return (float(i) + 0.5f) * float(srcSize) / float(outSize);
|
||||
}
|
||||
|
||||
// Which span holds t: 0 [0, lo), 1 [lo, hi), 2 [hi, size).
|
||||
inline int SpanIndex(float t, int32_t lo, int32_t hi)
|
||||
{
|
||||
return t < float(lo) ? 0 : t < float(hi) ? 1 : 2;
|
||||
}
|
||||
|
||||
// RCAS's neighbour j (i +- 1) of output pixel i: j, or i itself where
|
||||
// j's centre lies in another span (its span's edge pixel, as ClampTexel
|
||||
// keeps a tap), so the sharpening never reads across the band's edge.
|
||||
inline int32_t RcasNeighbour(int32_t i, int32_t j, int32_t lo, int32_t hi, int32_t outSize, int32_t srcSize)
|
||||
{
|
||||
j = std::clamp(j, 0, outSize - 1);
|
||||
return SpanIndex(SourceOf(j, outSize, srcSize), lo, hi) == SpanIndex(SourceOf(i, outSize, srcSize), lo, hi) ? j : i;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING). See fsr1.h.
|
||||
#include "fsr1.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
// AMD's headers (thirdparty/fsr1, MIT), as text for the shaders: k_ffx_a and
|
||||
// k_ffx_fsr1 (runtime/CMakeLists.txt).
|
||||
#include <fsr1_headers.inc>
|
||||
|
||||
// The same headers' CPU code, for the constants (it defines short type
|
||||
// names and many helpers this file doesn't use).
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-function"
|
||||
#pragma GCC diagnostic ignored "-Wunused-parameter"
|
||||
#define A_CPU 1
|
||||
#include <thirdparty/fsr1/ffx_a.h>
|
||||
#include <thirdparty/fsr1/ffx_fsr1.h>
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
namespace video::fsr1
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string Text(const unsigned char* bytes)
|
||||
{
|
||||
return std::string(reinterpret_cast<const char*>(bytes));
|
||||
}
|
||||
|
||||
// Both shaders: AMD's 32-bit GLSL path (no extensions needed).
|
||||
constexpr const char* kPrelude = R"(#version 460
|
||||
#define A_GPU 1
|
||||
#define A_GLSL 1
|
||||
)";
|
||||
|
||||
// EASU, one 8x8 group per 64 pixels of the picture. Its 12 taps come
|
||||
// from 4 gathers (FsrEasuRF..BF at the corners between texels);
|
||||
// under Hor+ and Vert+ (bandOn) each of a gather's 2x2 texels is
|
||||
// fetched instead and kept in this pixel's span of the band's three
|
||||
// along each axis (band_filter.h's SpanOf and ClampTexel), so a strip
|
||||
// pixel reads only the strip and a band pixel only the band.
|
||||
// band: the band's columns x..y and rows z..w in texels (video::BandOf).
|
||||
constexpr const char* kEasuHead = R"(
|
||||
layout(local_size_x = 8, local_size_y = 8) in;
|
||||
layout(set = 0, binding = 0) uniform sampler2D src;
|
||||
layout(set = 0, binding = 1, rgba8) uniform writeonly image2D dst;
|
||||
layout(push_constant) uniform P { uvec4 con0, con1, con2, con3; ivec4 band; ivec2 outSize; ivec2 srcSize; uint bandOn; } p;
|
||||
|
||||
// This pixel's span: its first and last texels along each axis.
|
||||
ivec2 spanLo = ivec2(0), spanHi = ivec2(0);
|
||||
|
||||
// band_filter.h's SpanOf: [0, lo), [lo, hi) or [hi, size) holding t, as its first and end texels.
|
||||
ivec2 spanOf(float t, int lo, int hi, int size)
|
||||
{
|
||||
return t < float(lo) ? ivec2(0, lo) : t < float(hi) ? ivec2(lo, hi) : ivec2(hi, size);
|
||||
}
|
||||
|
||||
vec3 texelIn(ivec2 t)
|
||||
{
|
||||
return texelFetch(src, clamp(t, spanLo, spanHi), 0).rgb;
|
||||
}
|
||||
|
||||
// What a gather at q reads, each texel kept in the span, in a gather's
|
||||
// order: (x0, y1), (x1, y1), (x1, y0), (x0, y0).
|
||||
vec4 quadIn(vec2 q, int c)
|
||||
{
|
||||
ivec2 t = ivec2(floor(q * vec2(p.srcSize) - 0.5));
|
||||
return vec4(texelIn(t + ivec2(0, 1))[c], texelIn(t + ivec2(1, 1))[c], texelIn(t + ivec2(1, 0))[c], texelIn(t)[c]);
|
||||
}
|
||||
|
||||
AF4 FsrEasuRF(AF2 q) { return p.bandOn == 0u ? textureGather(src, q, 0) : quadIn(q, 0); }
|
||||
AF4 FsrEasuGF(AF2 q) { return p.bandOn == 0u ? textureGather(src, q, 1) : quadIn(q, 1); }
|
||||
AF4 FsrEasuBF(AF2 q) { return p.bandOn == 0u ? textureGather(src, q, 2) : quadIn(q, 2); }
|
||||
#define FSR_EASU_F 1
|
||||
)";
|
||||
|
||||
constexpr const char* kEasuMain = R"(
|
||||
void main()
|
||||
{
|
||||
uvec2 ip = gl_GlobalInvocationID.xy;
|
||||
if (ip.x >= uint(p.outSize.x) || ip.y >= uint(p.outSize.y))
|
||||
return;
|
||||
if (p.bandOn != 0u)
|
||||
{
|
||||
// Where this pixel's centre falls in the frame image (texels).
|
||||
vec2 pos = (vec2(ip) + 0.5) * vec2(p.srcSize) / vec2(p.outSize);
|
||||
ivec2 sx = spanOf(pos.x, p.band.x, p.band.y, p.srcSize.x), sy = spanOf(pos.y, p.band.z, p.band.w, p.srcSize.y);
|
||||
spanLo = ivec2(sx.x, sy.x);
|
||||
spanHi = ivec2(sx.y, sy.y) - 1;
|
||||
}
|
||||
AF3 c;
|
||||
FsrEasuF(c, ip, p.con0, p.con1, p.con2, p.con3);
|
||||
imageStore(dst, ivec2(ip), vec4(c, 1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
// RCAS, drawn over the picture's place in the swapchain image (the
|
||||
// viewport at origin, outSize): src is EASU's image. Under Hor+ and
|
||||
// Vert+ a neighbour on the other side of the band's edge (its centre
|
||||
// maps into another span than this pixel's, by EASU's rule) reads
|
||||
// this pixel instead: the span's edge, as ClampTexel keeps a tap.
|
||||
// con.x 0 (Sharpening 0): EASU's picture as it is (RCAS's own
|
||||
// arithmetic would still scale it by its approximate reciprocal).
|
||||
constexpr const char* kRcasHead = R"(
|
||||
layout(location = 0) in vec2 uv;
|
||||
layout(location = 0) out vec4 color;
|
||||
layout(set = 0, binding = 0) uniform sampler2D src;
|
||||
layout(push_constant) uniform P { uvec4 con; ivec4 band; ivec2 origin; ivec2 outSize; ivec2 srcSize; uint bandOn; } p;
|
||||
|
||||
ivec2 centre = ivec2(0), centreSpan = ivec2(0);
|
||||
|
||||
// Which of the band's three spans along an axis holds output pixel i's centre: 0, 1 or 2.
|
||||
int spanIndex(int i, int lo, int hi, int outSize, int srcSize)
|
||||
{
|
||||
float t = (float(i) + 0.5) * float(srcSize) / float(outSize);
|
||||
return t < float(lo) ? 0 : t < float(hi) ? 1 : 2;
|
||||
}
|
||||
|
||||
AF4 FsrRcasLoadF(ASU2 q)
|
||||
{
|
||||
ivec2 t = clamp(q, ivec2(0), p.outSize - 1);
|
||||
if (p.bandOn != 0u)
|
||||
{
|
||||
if (spanIndex(t.x, p.band.x, p.band.y, p.outSize.x, p.srcSize.x) != centreSpan.x)
|
||||
t.x = centre.x;
|
||||
if (spanIndex(t.y, p.band.z, p.band.w, p.outSize.y, p.srcSize.y) != centreSpan.y)
|
||||
t.y = centre.y;
|
||||
}
|
||||
return texelFetch(src, t, 0);
|
||||
}
|
||||
void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {}
|
||||
#define FSR_RCAS_F 1
|
||||
)";
|
||||
|
||||
constexpr const char* kRcasMain = R"(
|
||||
void main()
|
||||
{
|
||||
centre = clamp(ivec2(gl_FragCoord.xy) - p.origin, ivec2(0), p.outSize - 1);
|
||||
if (p.bandOn != 0u)
|
||||
centreSpan = ivec2(spanIndex(centre.x, p.band.x, p.band.y, p.outSize.x, p.srcSize.x),
|
||||
spanIndex(centre.y, p.band.z, p.band.w, p.outSize.y, p.srcSize.y));
|
||||
if (p.con.x == 0u)
|
||||
{
|
||||
color = vec4(texelFetch(src, centre, 0).rgb, 1.0);
|
||||
return;
|
||||
}
|
||||
AF1 r, g, b;
|
||||
FsrRcasF(r, g, b, uvec2(centre), p.con);
|
||||
color = vec4(r, g, b, 1.0);
|
||||
}
|
||||
)";
|
||||
}
|
||||
|
||||
std::array<uint32_t, 16> EasuConstants(uint32_t srcW, uint32_t srcH, uint32_t dstW, uint32_t dstH)
|
||||
{
|
||||
std::array<uint32_t, 16> con{};
|
||||
FsrEasuCon(&con[0], &con[4], &con[8], &con[12], AF1(srcW), AF1(srcH), AF1(srcW), AF1(srcH), AF1(dstW), AF1(dstH));
|
||||
return con;
|
||||
}
|
||||
|
||||
std::array<uint32_t, 4> RcasConstants(float stops)
|
||||
{
|
||||
std::array<uint32_t, 4> con{};
|
||||
if (!std::isfinite(stops))
|
||||
return con; // off: con.x 0 (the shader then copies EASU's picture)
|
||||
FsrRcasCon(con.data(), AF1(stops));
|
||||
return con;
|
||||
}
|
||||
|
||||
std::string EasuGlsl()
|
||||
{
|
||||
return kPrelude + Text(k_ffx_a) + kEasuHead + Text(k_ffx_fsr1) + kEasuMain;
|
||||
}
|
||||
|
||||
std::string RcasGlsl()
|
||||
{
|
||||
return kPrelude + Text(k_ffx_a) + kRcasHead + Text(k_ffx_fsr1) + kRcasMain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Settings > Graphics > Upscaling's FSR 1: AMD FidelityFX Super Resolution 1
|
||||
// (runtime/thirdparty/fsr1, MIT, unmodified), as two passes after the
|
||||
// anti-aliasing (video/presenter.cpp):
|
||||
// - EASU (compute), the frame image enlarged into an image the size of
|
||||
// the picture on the screen: AMD's FsrEasuF (32-bit), whose 12 taps
|
||||
// come from 4 gathers, or under Hor+ and Vert+ from fetches kept in the
|
||||
// pixel's span of the 16:9 band's three (video/band_filter.h);
|
||||
// - RCAS (fragment, into the swapchain image, where bicubic's pass draws),
|
||||
// AMD's FsrRcasF: sharpening of that image, its 4 neighbours each kept
|
||||
// in the pixel's span too; then Brightness and the 360's display gamma
|
||||
// (the tone variant, as for the other scale shaders), then the UI.
|
||||
// Their constants come from AMD's own CPU code (FsrEasuCon, FsrRcasCon).
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace video::fsr1
|
||||
{
|
||||
// EASU's push constants: FsrEasuCon's con0..con3 for a srcW x srcH
|
||||
// image enlarged to dstW x dstH.
|
||||
std::array<uint32_t, 16> EasuConstants(uint32_t srcW, uint32_t srcH, uint32_t dstW, uint32_t dstH);
|
||||
// RCAS's: FsrRcasCon's con for a strength in stops below its maximum
|
||||
// (video::upscale::RcasStops; infinity gives 0: no sharpening).
|
||||
std::array<uint32_t, 4> RcasConstants(float stops);
|
||||
|
||||
// The shaders' GLSL: AMD's two headers between ours (the RCAS one has a
|
||||
// single main(), so the tone variant can wrap it).
|
||||
std::string EasuGlsl();
|
||||
std::string RcasGlsl();
|
||||
}
|
||||
+533
-32
@@ -19,6 +19,8 @@
|
||||
#include <cpu/guest_time.h>
|
||||
#include "vblank_lock.h"
|
||||
#include "display_tone.h"
|
||||
#include "fsr1.h"
|
||||
#include "upscale.h"
|
||||
#include <ui/ui.h>
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS
|
||||
#include <platform/ios_ui.h>
|
||||
@@ -102,12 +104,20 @@ namespace video
|
||||
VkDeviceSize stagingSize = 0;
|
||||
VkImageView sourceView = VK_NULL_HANDLE; // this frame's view of the presented image
|
||||
bool timed = false; // wrote its post-processing timestamps
|
||||
upscale::Pass pass = upscale::Pass::Bicubic; // the pass it drew with (NFSMW_UPSCALE_AB)
|
||||
} s_frames[FRAMES_IN_FLIGHT];
|
||||
// NFSMW_GPU_TIMING=1: 3 timestamps per frame slot around the
|
||||
// post-processing (before, after AA, after the scale and UI pass).
|
||||
VkQueryPool s_timestamps = VK_NULL_HANDLE;
|
||||
double s_timestampPeriod = 1.0;
|
||||
std::atomic<uint64_t> s_postFrames{ 0 }, s_postAaNs{ 0 }, s_postNs{ 0 };
|
||||
// NFSMW_UPSCALE_AB=1 (a diagnostic, with NFSMW_GPU_TIMING=1): frames
|
||||
// the FSR 1 setting enlarges alternate between FSR 1 and bicubic, one
|
||||
// frame each, and an [upscale-ab] line every 600 timed frames gives
|
||||
// each pass's post-processing GPU time per frame (anti-aliasing and
|
||||
// the UI included, as in [perf]): the same scenes, the same clocks.
|
||||
const bool s_upscaleAb = [] { const char* v = std::getenv("NFSMW_UPSCALE_AB"); return v && v[0] == '1'; }();
|
||||
uint64_t s_abFrames[3]{}, s_abNs[3]{}, s_abAaNs[3]{}; // by upscale::Pass; main thread
|
||||
std::vector<VkSemaphore> s_renderDone; // one per swapchain image
|
||||
std::mutex s_queueMutex;
|
||||
VulkanContext s_context{};
|
||||
@@ -1362,8 +1372,10 @@ void main()
|
||||
if (!EnsureScalePipeline())
|
||||
return false;
|
||||
s_post.ready = true;
|
||||
const int32_t mode = settings::GetInt(settings::Id::Scaling);
|
||||
fprintf(stderr, "[video] post-processing: %s, %s scaling%s%s\n", AaEnabled() ? "anti-aliasing" : "no anti-aliasing",
|
||||
Bicubic() ? "bicubic" : "linear", Bicubic() ? std::format(", sharpen {:.2f}", Sharpen()).c_str() : "",
|
||||
mode == int32_t(settings::ScalingMode::Fsr1) ? "FSR 1 (enlarging; else bicubic)" : Bicubic() ? "bicubic" : "linear",
|
||||
mode != int32_t(settings::ScalingMode::Linear) ? std::format(", sharpen {:.2f}", Sharpen()).c_str() : "",
|
||||
s_fastScale ? " (NFSMW_FAST_SCALE: 7 fetches)" : "");
|
||||
return true;
|
||||
}
|
||||
@@ -1546,6 +1558,487 @@ void main()
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Settings > Graphics > Upscaling's FSR 1 (video/fsr1.h; when it is
|
||||
// used: video/upscale.h): EASU (compute) from the anti-aliased frame
|
||||
// into s_fsr.image, the picture's size on the screen, then RCAS
|
||||
// (fragment) where the bicubic pass draws, with the tone table and
|
||||
// then the UI after it as before. Made the first time a frame is
|
||||
// enlarged with it; if that fails, those frames take the bicubic
|
||||
// pass (logged once).
|
||||
struct FsrState
|
||||
{
|
||||
bool tried = false, ready = false, toneTried = false;
|
||||
VkDescriptorSetLayout easuSet = VK_NULL_HANDLE, rcasSet = VK_NULL_HANDLE;
|
||||
VkPipelineLayout easuLayout = VK_NULL_HANDLE, rcasLayout = VK_NULL_HANDLE;
|
||||
VkPipeline easu = VK_NULL_HANDLE;
|
||||
VkShaderModule rcasFs = VK_NULL_HANDLE, rcasToneFs = VK_NULL_HANDLE;
|
||||
VkPipeline rcas = VK_NULL_HANDLE, rcasTone = VK_NULL_HANDLE;
|
||||
VkFormat rcasFormat = VK_FORMAT_UNDEFINED, rcasToneFormat = VK_FORMAT_UNDEFINED; // the swapchain's they were made for
|
||||
// EASU's picture, the size of the picture on the screen.
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
uint32_t width = 0, height = 0;
|
||||
} s_fsr;
|
||||
|
||||
// The shaders' push constants (their P in video/fsr1.cpp).
|
||||
struct EasuPush
|
||||
{
|
||||
uint32_t con[16]; // FsrEasuCon's con0..con3
|
||||
int32_t band[4]; // the 16:9 band's columns x0..x1 and rows y0..y1 (video::BandOf)
|
||||
int32_t outW, outH, srcW, srcH;
|
||||
uint32_t bandOn;
|
||||
};
|
||||
static_assert(sizeof(EasuPush) == 100);
|
||||
struct RcasPush
|
||||
{
|
||||
uint32_t con[4]; // FsrRcasCon's (con[0] 0: no sharpening)
|
||||
int32_t band[4];
|
||||
int32_t originX, originY, outW, outH, srcW, srcH;
|
||||
uint32_t bandOn;
|
||||
};
|
||||
static_assert(sizeof(RcasPush) == 60);
|
||||
|
||||
bool EnsureFsr()
|
||||
{
|
||||
if (s_fsr.tried)
|
||||
return s_fsr.ready;
|
||||
s_fsr.tried = true;
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
VkDescriptorSetLayoutBinding easuBindings[2] = {
|
||||
{ 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr },
|
||||
{ 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr },
|
||||
};
|
||||
// RCAS: EASU's picture, and the tone table (its tone variant).
|
||||
VkDescriptorSetLayoutBinding rcasBindings[2] = {
|
||||
{ 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr },
|
||||
{ 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr },
|
||||
};
|
||||
VkDescriptorSetLayoutCreateInfo dslci{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO };
|
||||
dslci.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR;
|
||||
dslci.bindingCount = 2;
|
||||
dslci.pBindings = easuBindings;
|
||||
VkDescriptorSetLayoutCreateInfo rcasDslci = dslci;
|
||||
rcasDslci.pBindings = rcasBindings;
|
||||
VkPushConstantRange easuRange{ VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(EasuPush) };
|
||||
VkPushConstantRange rcasRange{ VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(RcasPush) };
|
||||
VkPipelineLayoutCreateInfo plci{ VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO };
|
||||
plci.setLayoutCount = 1;
|
||||
plci.pSetLayouts = &s_fsr.easuSet;
|
||||
plci.pushConstantRangeCount = 1;
|
||||
plci.pPushConstantRanges = &easuRange;
|
||||
VkPipelineLayoutCreateInfo rcasPlci = plci;
|
||||
rcasPlci.pSetLayouts = &s_fsr.rcasSet;
|
||||
rcasPlci.pPushConstantRanges = &rcasRange;
|
||||
if (!Check(vkCreateDescriptorSetLayout(s_device, &dslci, nullptr, &s_fsr.easuSet), "FSR 1 EASU set layout") ||
|
||||
!Check(vkCreateDescriptorSetLayout(s_device, &rcasDslci, nullptr, &s_fsr.rcasSet), "FSR 1 RCAS set layout") ||
|
||||
!Check(vkCreatePipelineLayout(s_device, &plci, nullptr, &s_fsr.easuLayout), "FSR 1 EASU pipeline layout") ||
|
||||
!Check(vkCreatePipelineLayout(s_device, &rcasPlci, nullptr, &s_fsr.rcasLayout), "FSR 1 RCAS pipeline layout"))
|
||||
{
|
||||
fprintf(stderr, "[video] FSR 1 is off: its pipeline layouts couldn't be made; bicubic upscaling instead\n");
|
||||
return false;
|
||||
}
|
||||
VkShaderModule cs = PostModule(fsr1::EasuGlsl().c_str(), 2);
|
||||
s_fsr.rcasFs = PostModule(fsr1::RcasGlsl().c_str(), 1);
|
||||
if (!cs || !s_fsr.rcasFs)
|
||||
{
|
||||
fprintf(stderr, "[video] FSR 1 is off: its shaders didn't compile; bicubic upscaling instead\n");
|
||||
return false;
|
||||
}
|
||||
VkComputePipelineCreateInfo cpci{ VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO };
|
||||
cpci.stage = { VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_COMPUTE_BIT, cs, "main", nullptr };
|
||||
cpci.layout = s_fsr.easuLayout;
|
||||
const bool made = Check(vkCreateComputePipelines(s_device, VK_NULL_HANDLE, 1, &cpci, nullptr, &s_fsr.easu), "FSR 1 EASU pipeline");
|
||||
vkDestroyShaderModule(s_device, cs, nullptr);
|
||||
if (!made)
|
||||
{
|
||||
fprintf(stderr, "[video] FSR 1 is off: its EASU pipeline couldn't be made; bicubic upscaling instead\n");
|
||||
return false;
|
||||
}
|
||||
s_fsr.ready = true;
|
||||
fprintf(stderr, "[video] FSR 1: EASU and RCAS compiled in %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count());
|
||||
return true;
|
||||
}
|
||||
|
||||
// RCAS's pipeline for the swapchain's format, plain or with the tone
|
||||
// table (Brightness, the 360's display gamma); VK_NULL_HANDLE if it
|
||||
// can't be made (the frame then takes the bicubic pass).
|
||||
VkPipeline RcasPipeline(bool tone)
|
||||
{
|
||||
if (tone && !s_fsr.toneTried)
|
||||
{
|
||||
s_fsr.toneTried = true;
|
||||
const std::string glsl = ToneVariant(fsr1::RcasGlsl().c_str());
|
||||
s_fsr.rcasToneFs = glsl.empty() ? VK_NULL_HANDLE : PostModule(glsl.c_str(), 1);
|
||||
if (!s_fsr.rcasToneFs)
|
||||
fprintf(stderr, "[video] FSR 1: the RCAS shader with Brightness couldn't be made; bicubic upscaling while the tone is in use\n");
|
||||
}
|
||||
VkShaderModule fs = tone ? s_fsr.rcasToneFs : s_fsr.rcasFs;
|
||||
VkPipeline& pipeline = tone ? s_fsr.rcasTone : s_fsr.rcas;
|
||||
VkFormat& format = tone ? s_fsr.rcasToneFormat : s_fsr.rcasFormat;
|
||||
if (!fs)
|
||||
return VK_NULL_HANDLE;
|
||||
if (pipeline && format == s_swapFormat)
|
||||
return pipeline;
|
||||
if (pipeline)
|
||||
vkDestroyPipeline(s_device, pipeline, nullptr);
|
||||
pipeline = MakeScalePipeline(fs, s_fsr.rcasLayout);
|
||||
format = pipeline ? s_swapFormat : VK_FORMAT_UNDEFINED;
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
void EnsureFsrImage(uint32_t width, uint32_t height)
|
||||
{
|
||||
if (s_fsr.image && s_fsr.width == width && s_fsr.height == height)
|
||||
return;
|
||||
WaitIdle();
|
||||
if (s_fsr.image)
|
||||
{
|
||||
vkDestroyImageView(s_device, s_fsr.view, nullptr);
|
||||
vkDestroyImage(s_device, s_fsr.image, nullptr);
|
||||
vkFreeMemory(s_device, s_fsr.memory, nullptr);
|
||||
}
|
||||
VkImageCreateInfo ii{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
|
||||
ii.imageType = VK_IMAGE_TYPE_2D;
|
||||
ii.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
ii.extent = { width, height, 1 };
|
||||
ii.mipLevels = 1;
|
||||
ii.arrayLayers = 1;
|
||||
ii.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
ii.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
ii.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
vkCreateImage(s_device, &ii, nullptr, &s_fsr.image);
|
||||
VkMemoryRequirements req;
|
||||
vkGetImageMemoryRequirements(s_device, s_fsr.image, &req);
|
||||
VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||||
ai.allocationSize = req.size;
|
||||
ai.memoryTypeIndex = FindMemoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
vkAllocateMemory(s_device, &ai, nullptr, &s_fsr.memory);
|
||||
vkBindImageMemory(s_device, s_fsr.image, s_fsr.memory, 0);
|
||||
VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
|
||||
vci.image = s_fsr.image;
|
||||
vci.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
vci.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
|
||||
vkCreateImageView(s_device, &vci, nullptr, &s_fsr.view);
|
||||
s_fsr.width = width;
|
||||
s_fsr.height = height;
|
||||
}
|
||||
|
||||
// EASU: the w x h frame (scaleInput) enlarged to dw x dh in
|
||||
// s_fsr.image, ready for RCAS's reads. Outside a rendering pass.
|
||||
void RecordEasu(VkCommandBuffer cmd, VkImageView scaleInput, uint32_t w, uint32_t h, int32_t dw, int32_t dh,
|
||||
const video::FrameBand& band, bool bandOn)
|
||||
{
|
||||
EnsureFsrImage(uint32_t(dw), uint32_t(dh));
|
||||
// After the last frame's RCAS read it.
|
||||
Barrier(cmd, s_fsr.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_READ_BIT,
|
||||
VK_ACCESS_SHADER_WRITE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
|
||||
VkDescriptorImageInfo in{ s_post.sampler, scaleInput, VK_IMAGE_LAYOUT_GENERAL };
|
||||
VkDescriptorImageInfo out{ VK_NULL_HANDLE, s_fsr.view, VK_IMAGE_LAYOUT_GENERAL };
|
||||
VkWriteDescriptorSet writes[2] = {
|
||||
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, VK_NULL_HANDLE, 0, 0, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &in, nullptr, nullptr },
|
||||
{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, VK_NULL_HANDLE, 1, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &out, nullptr, nullptr },
|
||||
};
|
||||
EasuPush pc{};
|
||||
const std::array<uint32_t, 16> con = fsr1::EasuConstants(w, h, uint32_t(dw), uint32_t(dh));
|
||||
std::copy(con.begin(), con.end(), pc.con);
|
||||
pc.band[0] = band.x0;
|
||||
pc.band[1] = band.x1;
|
||||
pc.band[2] = band.y0;
|
||||
pc.band[3] = band.y1;
|
||||
pc.outW = dw;
|
||||
pc.outH = dh;
|
||||
pc.srcW = int32_t(w);
|
||||
pc.srcH = int32_t(h);
|
||||
pc.bandOn = bandOn ? 1u : 0u;
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s_fsr.easu);
|
||||
s_post.pushDescriptorSet(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, s_fsr.easuLayout, 0, 2, writes);
|
||||
vkCmdPushConstants(cmd, s_fsr.easuLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(pc), &pc);
|
||||
vkCmdDispatch(cmd, (uint32_t(dw) + 7) / 8, (uint32_t(dh) + 7) / 8, 1);
|
||||
Barrier(cmd, s_fsr.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_ACCESS_SHADER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
|
||||
}
|
||||
|
||||
// Inside a rendering pass on the swapchain image (or a capture's
|
||||
// image of its format): the picture at dx, dy, dw x dh, by `pass`
|
||||
// (FSR 1: RCAS over RecordEasu's image, with `rcas` its pipeline),
|
||||
// through the tone table when `tone`.
|
||||
void DrawPicture(VkCommandBuffer cmd, upscale::Pass pass, VkPipeline rcas, bool tone, VkImageView scaleInput, uint32_t w,
|
||||
uint32_t h, const video::FrameBand& band, bool bandOn, int32_t dx, int32_t dy, int32_t dw, int32_t dh, float sharpen)
|
||||
{
|
||||
VkViewport viewport{ float(dx), float(dy), float(dw), float(dh), 0.0f, 1.0f };
|
||||
VkRect2D scissor{ { dx, dy }, { uint32_t(dw), uint32_t(dh) } };
|
||||
vkCmdSetViewport(cmd, 0, 1, &viewport);
|
||||
vkCmdSetScissor(cmd, 0, 1, &scissor);
|
||||
const bool fsr = pass == upscale::Pass::Fsr1;
|
||||
const VkPipelineLayout layout = fsr ? s_fsr.rcasLayout : tone ? s_tone.layout : s_post.scaleLayout;
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, fsr ? rcas : tone ? s_tone.pipeline : s_post.scale);
|
||||
VkDescriptorImageInfo in{ s_post.sampler, fsr ? s_fsr.view : scaleInput, VK_IMAGE_LAYOUT_GENERAL };
|
||||
VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, VK_NULL_HANDLE, 0, 0, 1,
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &in, nullptr, nullptr };
|
||||
if (tone)
|
||||
{
|
||||
VkDescriptorImageInfo table{ s_post.sampler, s_tone.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
|
||||
VkWriteDescriptorSet writes[2] = { write, write };
|
||||
writes[1].dstBinding = 1;
|
||||
writes[1].pImageInfo = &table;
|
||||
s_post.pushDescriptorSet(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 2, writes);
|
||||
}
|
||||
else
|
||||
s_post.pushDescriptorSet(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &write);
|
||||
if (fsr)
|
||||
{
|
||||
RcasPush pc{};
|
||||
const std::array<uint32_t, 4> con = fsr1::RcasConstants(upscale::RcasStops(sharpen));
|
||||
std::copy(con.begin(), con.end(), pc.con);
|
||||
pc.band[0] = band.x0;
|
||||
pc.band[1] = band.x1;
|
||||
pc.band[2] = band.y0;
|
||||
pc.band[3] = band.y1;
|
||||
pc.originX = dx;
|
||||
pc.originY = dy;
|
||||
pc.outW = dw;
|
||||
pc.outH = dh;
|
||||
pc.srcW = int32_t(w);
|
||||
pc.srcH = int32_t(h);
|
||||
pc.bandOn = bandOn ? 1u : 0u;
|
||||
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc);
|
||||
}
|
||||
else
|
||||
{
|
||||
struct
|
||||
{
|
||||
float width, height, sharpen;
|
||||
uint32_t bicubic;
|
||||
float band[4];
|
||||
uint32_t bandOn;
|
||||
} pc = { float(w), float(h), sharpen, pass == upscale::Pass::Bicubic ? 1u : 0u,
|
||||
{ float(band.x0), float(band.x1), float(band.y0), float(band.y1) }, bandOn ? 1u : 0u };
|
||||
static_assert(sizeof(pc) == 36);
|
||||
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc);
|
||||
}
|
||||
vkCmdDraw(cmd, 3, 1, 0, 0);
|
||||
}
|
||||
|
||||
// The pass a frame takes (video/upscale.h), FSR 1 only where its
|
||||
// pipelines are ready (made here the first time); a line in the log
|
||||
// when the pass or the sizes change.
|
||||
upscale::Pass ChoosePass(bool tone, uint32_t w, uint32_t h, int32_t dw, int32_t dh, VkPipeline& rcas)
|
||||
{
|
||||
rcas = VK_NULL_HANDLE;
|
||||
upscale::Pass pass = upscale::Choose(settings::GetInt(settings::Id::Scaling), w, h, dw, dh);
|
||||
if (pass == upscale::Pass::Fsr1 && EnsureFsr())
|
||||
rcas = RcasPipeline(tone);
|
||||
if (pass == upscale::Pass::Fsr1 && !rcas)
|
||||
pass = upscale::Pass::Bicubic;
|
||||
static upscale::Pass lastPass = upscale::Pass::Linear;
|
||||
static uint32_t lastW = 0, lastH = 0;
|
||||
static int32_t lastDw = 0, lastDh = 0;
|
||||
static int lines = 0;
|
||||
if ((pass != lastPass || w != lastW || h != lastH || dw != lastDw || dh != lastDh) && lines < 32)
|
||||
{
|
||||
lines++;
|
||||
const bool fsrChosen = settings::GetInt(settings::Id::Scaling) == int32_t(settings::ScalingMode::Fsr1);
|
||||
fprintf(stderr, "[video] upscaling: %s, %ux%u -> %dx%d%s\n", upscale::PassName(pass), w, h, dw, dh,
|
||||
pass == upscale::Pass::Fsr1 ? std::format(" (RCAS at {:.2f} of its strength)", upscale::RcasAmount(Sharpen())).c_str()
|
||||
: !fsrChosen ? ""
|
||||
: upscale::Enlarged(w, h, dw, dh) ? " (FSR 1 unavailable)" : " (FSR 1 only enlarges)");
|
||||
}
|
||||
lastPass = pass;
|
||||
lastW = w;
|
||||
lastH = h;
|
||||
lastDw = dw;
|
||||
lastDh = dh;
|
||||
return pass;
|
||||
}
|
||||
|
||||
// NFSMW_UPSCALE_COMPARE=<passes> (a diagnostic): with each
|
||||
// NFSMW_CHECK_PRESENT_SEC capture, the same frame drawn again by each
|
||||
// listed pass into an image of its own (the swapchain's size and
|
||||
// format, without the UI), saved as
|
||||
// build/present_check_<k>_<name>.ppm: side-by-sides of one frame.
|
||||
// Passes: bicubic, linear, fsr1, each at Sharpening or with
|
||||
// :<sharpening> (fsr1:0 is EASU alone). fsr1 where the frame isn't
|
||||
// enlarged draws bicubic, as the setting does. "source": the frame
|
||||
// the passes start from (after the anti-aliasing), at its own size
|
||||
// (through the tone table when that is in use).
|
||||
struct CompareEntry
|
||||
{
|
||||
std::string name;
|
||||
int32_t mode;
|
||||
float sharpen; // < 0: Sharpening's
|
||||
bool source = false;
|
||||
};
|
||||
const std::vector<CompareEntry>& CompareList()
|
||||
{
|
||||
static const std::vector<CompareEntry> list = [] {
|
||||
std::vector<CompareEntry> entries;
|
||||
const char* v = std::getenv("NFSMW_UPSCALE_COMPARE");
|
||||
std::string text = v ? v : "";
|
||||
for (size_t at = 0; at < text.size();)
|
||||
{
|
||||
size_t end = text.find(',', at);
|
||||
std::string item = text.substr(at, end == std::string::npos ? std::string::npos : end - at);
|
||||
at = end == std::string::npos ? text.size() : end + 1;
|
||||
std::string kind = item.substr(0, item.find(':'));
|
||||
float sharpen = item.find(':') == std::string::npos ? -1.0f : std::strtof(item.c_str() + item.find(':') + 1, nullptr);
|
||||
int32_t mode = kind == "fsr1" ? int32_t(upscale::Mode::Fsr1) : kind == "linear" || kind == "source" ? int32_t(upscale::Mode::Linear)
|
||||
: kind == "bicubic" ? int32_t(upscale::Mode::Bicubic) : -1;
|
||||
if (mode < 0)
|
||||
{
|
||||
fprintf(stderr, "[video] NFSMW_UPSCALE_COMPARE: \"%s\" isn't bicubic, linear, fsr1[:<sharpening>] or source\n", item.c_str());
|
||||
continue;
|
||||
}
|
||||
std::string name = item;
|
||||
std::replace(name.begin(), name.end(), ':', '-');
|
||||
entries.push_back({ name, mode, sharpen, kind == "source" });
|
||||
}
|
||||
return entries;
|
||||
}();
|
||||
return list;
|
||||
}
|
||||
|
||||
struct CompareShot
|
||||
{
|
||||
std::string name;
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkBuffer buffer = VK_NULL_HANDLE;
|
||||
VkDeviceMemory bufferMemory = VK_NULL_HANDLE;
|
||||
uint32_t width = 0, height = 0;
|
||||
};
|
||||
|
||||
// Records each listed pass's picture of this frame and its copy to
|
||||
// the host. Outside a rendering pass.
|
||||
std::vector<CompareShot> RecordCompare(VkCommandBuffer cmd, bool tone, VkImageView scaleInput, uint32_t w, uint32_t h,
|
||||
const video::FrameBand& band, bool bandOn, int32_t dx, int32_t dy, int32_t dw, int32_t dh)
|
||||
{
|
||||
std::vector<CompareShot> shots;
|
||||
for (const CompareEntry& entry : CompareList())
|
||||
{
|
||||
CompareShot shot;
|
||||
shot.name = entry.name;
|
||||
// The source: the frame image 1:1 (a bilinear fetch at each texel's centre).
|
||||
shot.width = entry.source ? w : s_swapExtent.width;
|
||||
shot.height = entry.source ? h : s_swapExtent.height;
|
||||
const int32_t x = entry.source ? 0 : dx, y = entry.source ? 0 : dy;
|
||||
const int32_t width = entry.source ? int32_t(w) : dw, height = entry.source ? int32_t(h) : dh;
|
||||
VkImageCreateInfo ii{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
|
||||
ii.imageType = VK_IMAGE_TYPE_2D;
|
||||
ii.format = s_swapFormat;
|
||||
ii.extent = { shot.width, shot.height, 1 };
|
||||
ii.mipLevels = 1;
|
||||
ii.arrayLayers = 1;
|
||||
ii.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
ii.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
ii.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
if (vkCreateImage(s_device, &ii, nullptr, &shot.image) != VK_SUCCESS)
|
||||
break;
|
||||
VkMemoryRequirements req;
|
||||
vkGetImageMemoryRequirements(s_device, shot.image, &req);
|
||||
VkMemoryAllocateInfo ai{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||||
ai.allocationSize = req.size;
|
||||
ai.memoryTypeIndex = FindMemoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
vkAllocateMemory(s_device, &ai, nullptr, &shot.memory);
|
||||
vkBindImageMemory(s_device, shot.image, shot.memory, 0);
|
||||
VkImageViewCreateInfo vci{ VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO };
|
||||
vci.image = shot.image;
|
||||
vci.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
vci.format = s_swapFormat;
|
||||
vci.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
|
||||
vkCreateImageView(s_device, &vci, nullptr, &shot.view);
|
||||
VkBufferCreateInfo bci{ VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
|
||||
bci.size = VkDeviceSize(shot.width) * shot.height * 4;
|
||||
bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
vkCreateBuffer(s_device, &bci, nullptr, &shot.buffer);
|
||||
vkGetBufferMemoryRequirements(s_device, shot.buffer, &req);
|
||||
ai.allocationSize = req.size;
|
||||
ai.memoryTypeIndex = FindMemoryType(req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
|
||||
vkAllocateMemory(s_device, &ai, nullptr, &shot.bufferMemory);
|
||||
vkBindBufferMemory(s_device, shot.buffer, shot.bufferMemory, 0);
|
||||
|
||||
VkPipeline rcas = VK_NULL_HANDLE;
|
||||
upscale::Pass pass = upscale::Choose(entry.mode, w, h, width, height);
|
||||
if (pass == upscale::Pass::Fsr1 && EnsureFsr())
|
||||
rcas = RcasPipeline(tone);
|
||||
if (pass == upscale::Pass::Fsr1 && !rcas)
|
||||
pass = upscale::Pass::Bicubic;
|
||||
if (pass == upscale::Pass::Fsr1)
|
||||
RecordEasu(cmd, scaleInput, w, h, width, height, band, bandOn);
|
||||
Barrier(cmd, shot.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
|
||||
VkRenderingAttachmentInfoKHR color{ VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR };
|
||||
color.imageView = shot.view;
|
||||
color.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
||||
VkRenderingInfoKHR ri{ VK_STRUCTURE_TYPE_RENDERING_INFO_KHR };
|
||||
ri.renderArea = { { 0, 0 }, { shot.width, shot.height } };
|
||||
ri.layerCount = 1;
|
||||
ri.colorAttachmentCount = 1;
|
||||
ri.pColorAttachments = &color;
|
||||
s_context.cmdBeginRendering(cmd, &ri);
|
||||
DrawPicture(cmd, pass, rcas, tone, scaleInput, w, h, band, bandOn, x, y, width, height,
|
||||
entry.sharpen >= 0.0f ? std::min(entry.sharpen, 1.0f) : Sharpen());
|
||||
s_context.cmdEndRendering(cmd);
|
||||
Barrier(cmd, shot.image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||
VkBufferImageCopy region{};
|
||||
region.imageSubresource = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1 };
|
||||
region.imageExtent = { shot.width, shot.height, 1 };
|
||||
vkCmdCopyImageToBuffer(cmd, shot.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, shot.buffer, 1, ®ion);
|
||||
VkBufferMemoryBarrier hostRead{ VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER };
|
||||
hostRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
hostRead.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
hostRead.srcQueueFamilyIndex = hostRead.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
hostRead.buffer = shot.buffer;
|
||||
hostRead.size = VK_WHOLE_SIZE;
|
||||
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &hostRead, 0, nullptr);
|
||||
fprintf(stderr, "[video] NFSMW_UPSCALE_COMPARE: %s drawn by %s\n", entry.name.c_str(), upscale::PassName(pass));
|
||||
shots.push_back(shot);
|
||||
}
|
||||
return shots;
|
||||
}
|
||||
|
||||
// After the frame's fence: RecordCompare's pictures saved and freed.
|
||||
void SaveCompare(std::vector<CompareShot>& shots, int captureIndex)
|
||||
{
|
||||
const bool bgr = s_swapFormat == VK_FORMAT_B8G8R8A8_UNORM || s_swapFormat == VK_FORMAT_B8G8R8A8_SRGB;
|
||||
for (CompareShot& shot : shots)
|
||||
{
|
||||
void* mapped = nullptr;
|
||||
const size_t pixels = size_t(shot.width) * shot.height;
|
||||
if (vkMapMemory(s_device, shot.bufferMemory, 0, VkDeviceSize(pixels) * 4, 0, &mapped) == VK_SUCCESS)
|
||||
{
|
||||
std::string path = std::format("build/present_check_{}_{}.ppm", captureIndex, shot.name);
|
||||
if (FILE* out = fopen(path.c_str(), "wb"))
|
||||
{
|
||||
fprintf(out, "P6\n%u %u\n255\n", shot.width, shot.height);
|
||||
const uint8_t* px = static_cast<const uint8_t*>(mapped);
|
||||
std::vector<uint8_t> rgb(pixels * 3);
|
||||
for (size_t i = 0; i < pixels; i++)
|
||||
for (int k = 0; k < 3; k++)
|
||||
rgb[i * 3 + k] = px[i * 4 + (bgr ? 2 - k : k)];
|
||||
fwrite(rgb.data(), 1, rgb.size(), out);
|
||||
fclose(out);
|
||||
fprintf(stderr, "[video] NFSMW_UPSCALE_COMPARE: saved %s\n", path.c_str());
|
||||
}
|
||||
vkUnmapMemory(s_device, shot.bufferMemory);
|
||||
}
|
||||
vkDestroyImageView(s_device, shot.view, nullptr);
|
||||
vkDestroyImage(s_device, shot.image, nullptr);
|
||||
vkFreeMemory(s_device, shot.memory, nullptr);
|
||||
vkDestroyBuffer(s_device, shot.buffer, nullptr);
|
||||
vkFreeMemory(s_device, shot.bufferMemory, nullptr);
|
||||
}
|
||||
shots.clear();
|
||||
}
|
||||
|
||||
// Xenia's Tiled2D for 32x32-aligned pitches (texture_address.h).
|
||||
uint32_t Tiled2D(uint32_t x, uint32_t y, uint32_t pitchAligned, uint32_t bppLog2)
|
||||
{
|
||||
@@ -2762,6 +3255,26 @@ void main()
|
||||
s_postFrames++;
|
||||
s_postAaNs += uint64_t(double(ts[1] - ts[0]) * s_timestampPeriod);
|
||||
s_postNs += uint64_t(double(ts[2] - ts[0]) * s_timestampPeriod);
|
||||
if (s_upscaleAb)
|
||||
{
|
||||
const int k = int(f.pass);
|
||||
s_abFrames[k]++;
|
||||
s_abNs[k] += uint64_t(double(ts[2] - ts[0]) * s_timestampPeriod);
|
||||
s_abAaNs[k] += uint64_t(double(ts[1] - ts[0]) * s_timestampPeriod);
|
||||
if ((s_abFrames[0] + s_abFrames[1] + s_abFrames[2]) % 600 == 0)
|
||||
{
|
||||
std::string text;
|
||||
for (upscale::Pass pass : { upscale::Pass::Bicubic, upscale::Pass::Fsr1, upscale::Pass::Linear })
|
||||
{
|
||||
const int i = int(pass);
|
||||
if (s_abFrames[i])
|
||||
text += std::format(" | {}: {:.3f} ms (AA {:.3f} ms) over {} frames", upscale::PassName(pass),
|
||||
double(s_abNs[i]) / 1e6 / double(s_abFrames[i]), double(s_abAaNs[i]) / 1e6 / double(s_abFrames[i]), s_abFrames[i]);
|
||||
s_abFrames[i] = s_abNs[i] = s_abAaNs[i] = 0;
|
||||
}
|
||||
fprintf(stderr, "[upscale-ab] post-processing GPU per frame%s\n", text.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
f.timed = false;
|
||||
}
|
||||
@@ -2838,6 +3351,7 @@ void main()
|
||||
VkDeviceMemory captureMemory = VK_NULL_HANDLE;
|
||||
VkDeviceSize captureSize = 0;
|
||||
int captureIndex = -1;
|
||||
std::vector<CompareShot> compareShots; // NFSMW_UPSCALE_COMPARE
|
||||
// Letterbox to the source aspect ratio; or, on a screen of another
|
||||
// shape with Aspect Ratio on Auto, fill it (video/picture_fit): the
|
||||
// game draws that frame with its view widened (a wider screen, Hor+)
|
||||
@@ -2963,6 +3477,16 @@ void main()
|
||||
// with the gamma off, and then the scale pass below is exactly as
|
||||
// before.
|
||||
const bool tone = scaleInput && PrepareTone(cmd);
|
||||
// The pass that draws the picture (video/upscale.h): FSR 1's
|
||||
// EASU first, outside the swapchain image's pass.
|
||||
VkPipeline rcas = VK_NULL_HANDLE;
|
||||
upscale::Pass pass = scaleInput ? ChoosePass(tone, w, h, dw, dh, rcas) : upscale::Pass::Bicubic;
|
||||
static uint64_t abCount = 0;
|
||||
if (s_upscaleAb && pass == upscale::Pass::Fsr1 && (abCount++ & 1))
|
||||
pass = upscale::Pass::Bicubic;
|
||||
f.pass = pass;
|
||||
if (scaleInput && pass == upscale::Pass::Fsr1)
|
||||
RecordEasu(cmd, scaleInput, w, h, dw, dh, band, bandOn);
|
||||
Barrier(cmd, dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0,
|
||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
|
||||
VkRenderingAttachmentInfoKHR color{ VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR };
|
||||
@@ -2980,37 +3504,7 @@ void main()
|
||||
s_context.cmdBeginRendering(cmd, &ri);
|
||||
if (scaleInput)
|
||||
{
|
||||
VkViewport viewport{ float(dx), float(dy), float(dw), float(dh), 0.0f, 1.0f };
|
||||
VkRect2D scissor{ { dx, dy }, { uint32_t(dw), uint32_t(dh) } };
|
||||
vkCmdSetViewport(cmd, 0, 1, &viewport);
|
||||
vkCmdSetScissor(cmd, 0, 1, &scissor);
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, tone ? s_tone.pipeline : s_post.scale);
|
||||
VkDescriptorImageInfo in{ s_post.sampler, scaleInput, VK_IMAGE_LAYOUT_GENERAL };
|
||||
VkWriteDescriptorSet write{ VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, VK_NULL_HANDLE, 0, 0, 1,
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &in, nullptr, nullptr };
|
||||
const VkPipelineLayout scaleLayout = tone ? s_tone.layout : s_post.scaleLayout;
|
||||
if (tone)
|
||||
{
|
||||
VkDescriptorImageInfo table{ s_post.sampler, s_tone.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL };
|
||||
VkWriteDescriptorSet writes[2] = { write, write };
|
||||
writes[1].dstBinding = 1;
|
||||
writes[1].pImageInfo = &table;
|
||||
s_post.pushDescriptorSet(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, scaleLayout, 0, 2, writes);
|
||||
}
|
||||
else
|
||||
s_post.pushDescriptorSet(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, s_post.scaleLayout, 0, 1, &write);
|
||||
bool bicubic = Bicubic();
|
||||
struct
|
||||
{
|
||||
float width, height, sharpen;
|
||||
uint32_t bicubic;
|
||||
float band[4];
|
||||
uint32_t bandOn;
|
||||
} pc = { float(w), float(h), Sharpen(), bicubic ? 1u : 0u, { float(band.x0), float(band.x1), float(band.y0), float(band.y1) },
|
||||
bandOn ? 1u : 0u };
|
||||
static_assert(sizeof(pc) == 36);
|
||||
vkCmdPushConstants(cmd, scaleLayout, VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc);
|
||||
vkCmdDraw(cmd, 3, 1, 0, 0);
|
||||
DrawPicture(cmd, pass, rcas, tone, scaleInput, w, h, band, bandOn, dx, dy, dw, dh, Sharpen());
|
||||
if (!captures.empty() && pictureRect.extent.width)
|
||||
{
|
||||
// The game's picture alone: pause the pass, copy it out,
|
||||
@@ -3099,6 +3593,8 @@ void main()
|
||||
else
|
||||
Barrier(cmd, dst, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
|
||||
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT);
|
||||
if (captureIndex >= 0 && scaleInput)
|
||||
compareShots = RecordCompare(cmd, tone, scaleInput, w, h, band, bandOn, dx, dy, dw, dh);
|
||||
if (post && !gpuImage)
|
||||
Barrier(cmd, s_source, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_SHADER_READ_BIT,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
|
||||
@@ -3275,6 +3771,11 @@ void main()
|
||||
vkDestroyBuffer(s_device, captureBuffer, nullptr);
|
||||
vkFreeMemory(s_device, captureMemory, nullptr);
|
||||
}
|
||||
if (!compareShots.empty())
|
||||
{
|
||||
vkWaitForFences(s_device, 1, &f.inFlight, VK_TRUE, UINT64_MAX);
|
||||
SaveCompare(compareShots, captureIndex);
|
||||
}
|
||||
// The next frame's CheckSwapchain builds again (before its acquire).
|
||||
if (present == VK_ERROR_OUT_OF_DATE_KHR || present == VK_SUBOPTIMAL_KHR)
|
||||
s_swapPolicy.Reported(present == VK_ERROR_OUT_OF_DATE_KHR ? SurfaceResult::OutOfDate : SurfaceResult::Suboptimal);
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// SpeedBreaker runtime. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Which pass draws the game's picture into the window (video/presenter.cpp),
|
||||
// and Settings > Graphics > Sharpening as FSR 1's RCAS strength. Pure
|
||||
// functions, for tests/upscale_test.cpp.
|
||||
//
|
||||
// Settings > Graphics > Upscaling: Bicubic (Catmull-Rom, sharpened and
|
||||
// clamped to the 2x2 texels around each sample), Bilinear, or FSR 1 (AMD
|
||||
// FidelityFX Super Resolution 1: EASU, an edge-adaptive Lanczos-like
|
||||
// upscale, then RCAS, a contrast-adaptive sharpening pass; video/fsr1.h).
|
||||
// FSR 1 is made for enlarging (AMD: 1x to 4x the pixels): it is used only
|
||||
// when the picture on the screen is larger than the frame image, along one
|
||||
// axis or both, and smaller along neither (1x Internal Resolution on a
|
||||
// 1080p, 1440p or MacBook screen; the Steam Deck's 720 rows stretched to
|
||||
// 800). A frame shown at its own size, or shrunk (2x on a 1080p screen),
|
||||
// takes the bicubic pass exactly as before FSR 1 existed.
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace video::upscale
|
||||
{
|
||||
// Settings > Graphics > Upscaling's values (settings::ScalingMode).
|
||||
enum class Mode : int32_t { Bicubic = 0, Linear = 1, Fsr1 = 2 };
|
||||
|
||||
enum class Pass : uint8_t { Linear, Bicubic, Fsr1 };
|
||||
|
||||
inline const char* PassName(Pass pass)
|
||||
{
|
||||
return pass == Pass::Fsr1 ? "FSR 1" : pass == Pass::Bicubic ? "bicubic" : "bilinear";
|
||||
}
|
||||
|
||||
// The picture enlarged: dstW x dstH (its place on the screen) is larger
|
||||
// than the srcW x srcH image along one axis and smaller along neither.
|
||||
inline bool Enlarged(uint32_t srcW, uint32_t srcH, int32_t dstW, int32_t dstH)
|
||||
{
|
||||
if (!srcW || !srcH || dstW <= 0 || dstH <= 0)
|
||||
return false;
|
||||
const bool notSmaller = uint32_t(dstW) >= srcW && uint32_t(dstH) >= srcH;
|
||||
return notSmaller && (uint32_t(dstW) > srcW || uint32_t(dstH) > srcH);
|
||||
}
|
||||
|
||||
// The pass for this frame. An unknown mode is bicubic, the default.
|
||||
inline Pass Choose(int32_t mode, uint32_t srcW, uint32_t srcH, int32_t dstW, int32_t dstH)
|
||||
{
|
||||
switch (Mode(mode))
|
||||
{
|
||||
case Mode::Linear:
|
||||
return Pass::Linear;
|
||||
case Mode::Fsr1:
|
||||
return Enlarged(srcW, srcH, dstW, dstH) ? Pass::Fsr1 : Pass::Bicubic;
|
||||
default:
|
||||
return Pass::Bicubic;
|
||||
}
|
||||
}
|
||||
|
||||
// Sharpening (0..1, default 0.3) for RCAS: the strength AMD's FsrRcasCon
|
||||
// takes as stops below its maximum (0: the most it sharpens, each stop
|
||||
// half as much), here as the linear factor it becomes (2^-stops), which
|
||||
// multiplies RCAS's sharpening lobe: Sharpening 1 is RCAS's maximum,
|
||||
// 0.5 half of it, and 0 turns RCAS off (EASU's picture as it is), as 0
|
||||
// turns bicubic's sharpening off.
|
||||
inline float RcasAmount(float sharpening)
|
||||
{
|
||||
if (!(sharpening > 0.0f)) // 0, negative or NaN
|
||||
return 0.0f;
|
||||
return std::min(sharpening, 1.0f);
|
||||
}
|
||||
|
||||
// The same as FsrRcasCon's stops (infinity: off).
|
||||
inline float RcasStops(float sharpening)
|
||||
{
|
||||
const float amount = RcasAmount(sharpening);
|
||||
return amount > 0.0f ? -std::log2(amount) : INFINITY;
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ MAC = platform.system() == 'Darwin'
|
||||
RACE = '8-26/2:START,30-130/4:DOWN,31-131/4:A,32.5-132.5/4:START,145:A'
|
||||
|
||||
# The menu's pages and rows (runtime/user/settings.cpp, hidden rows left out).
|
||||
PAGES = ['Display', 'Graphics', 'Audio', 'Input', 'Advanced']
|
||||
PAGES = ['Display', 'Graphics', 'Audio', 'Input', 'Controls', 'Advanced']
|
||||
ROWS = {
|
||||
'Display': ['video.fullscreen', 'video.vsync', 'video.frame_rate', 'video.aspect_ratio', 'video.brightness',
|
||||
'video.xbox_display_gamma'],
|
||||
|
||||
+38
-1
@@ -44,8 +44,33 @@ sb_test(gpu_choice_test)
|
||||
# video::FitPicture's pictures.
|
||||
sb_test(glare_fit_test SOURCES ${RT}/video/picture_fit.cpp)
|
||||
# The post-processing's split at the 16:9 band's edge (video/band_filter.h,
|
||||
# video::BandOf) against the edge-bar fills and a model of the filters.
|
||||
# video::BandOf) against the edge-bar fills and a model of the filters
|
||||
# (FSR 1's EASU taps and RCAS neighbours too).
|
||||
sb_test(band_filter_test SOURCES ${RT}/video/picture_fit.cpp)
|
||||
# Settings > Graphics > Upscaling's FSR 1: which pass draws a frame
|
||||
# (video/upscale.h, header only, on video::FitPicture's pictures), Sharpening
|
||||
# as RCAS's strength, the constants against AMD's formulas, and the shaders'
|
||||
# GLSL (video/fsr1.cpp, with runtime/generated/fsr1_headers.inc): AMD's
|
||||
# headers compiled in unchanged, the band rules in their code, the
|
||||
# presenter's use of them. It writes the GLSL to fsr1_glsl for the check
|
||||
# after it.
|
||||
sb_test(fsr1_test SOURCES ${RT}/video/fsr1.cpp ${RT}/video/picture_fit.cpp
|
||||
ARGS ${RT}/thirdparty/fsr1 ${RT}/video ${CMAKE_CURRENT_BINARY_DIR}/fsr1_glsl)
|
||||
target_include_directories(fsr1_test PRIVATE ${CMAKE_BINARY_DIR}/runtime/generated)
|
||||
# Those shaders compiled to SPIR-V for Vulkan, as the game's glslang does
|
||||
# (only where glslangValidator is installed).
|
||||
find_program(SB_GLSLANG_VALIDATOR glslangValidator)
|
||||
if(SB_GLSLANG_VALIDATOR)
|
||||
foreach(shader easu.comp rcas.frag rcas_tone.frag)
|
||||
add_test(NAME fsr1_glsl_check_${shader} COMMAND ${SB_GLSLANG_VALIDATOR} -V --target-env vulkan1.1
|
||||
${CMAKE_CURRENT_BINARY_DIR}/fsr1_glsl/${shader} -o ${CMAKE_CURRENT_BINARY_DIR}/fsr1_glsl/${shader}.spv)
|
||||
set_tests_properties(fsr1_glsl_check_${shader} PROPERTIES DEPENDS fsr1_test)
|
||||
endforeach()
|
||||
endif()
|
||||
# Upscaling's values in settings.toml (user/settings.cpp as the game links
|
||||
# it): "fsr1" read, saved and read back, the environment's override, an
|
||||
# unknown value; in a scratch folder.
|
||||
sb_test(settings_scaling_test SOURCES ${RT}/user/settings.cpp ARGS ${CMAKE_CURRENT_BINARY_DIR}/settings_scaling_scratch)
|
||||
# Internal Resolution Auto's budget on iPhones and iPads, chip by chip
|
||||
# (video/scale_budget.cpp), and the scale it makes on the phones' screens.
|
||||
sb_test(scale_budget_test SOURCES ${RT}/video/scale_budget.cpp ${RT}/video/picture_fit.cpp)
|
||||
@@ -137,6 +162,18 @@ sb_test(memory_status_test SOURCES ${RT}/kernel/memory_status.cpp ${RT}/platform
|
||||
# The version line in the log, and when the "What's new" note shows
|
||||
# (user/version_check.h, header only).
|
||||
sb_test(version_check_test)
|
||||
# The keys a Controls row can hold and their names in settings.toml
|
||||
# (user/key_names.h, header only), each row against SDL's scancode name.
|
||||
find_package(SDL3 REQUIRED CONFIG)
|
||||
sb_test(key_names_test)
|
||||
target_link_libraries(key_names_test PRIVATE SDL3::SDL3)
|
||||
# The Key options (Settings > Controls): the table, the [keys] names in the
|
||||
# file, and what Set accepts (user/settings.cpp and the toml++ XenonRecomp
|
||||
# vendors; no SDL).
|
||||
sb_test(settings_keys_test SOURCES ${RT}/user/settings.cpp)
|
||||
target_include_directories(settings_keys_test SYSTEM PRIVATE ${ROOT}/tools/XenonRecomp/thirdparty/tomlplusplus/include)
|
||||
# The keyboard's controls and how held keys become a pad (hid/key_map.h), header only.
|
||||
sb_test(key_map_test)
|
||||
# Check for Updates' logic: versions, GitHub's JSON, curl's output and the
|
||||
# release notes (update/check.cpp, self-contained).
|
||||
sb_test(update_check_test SOURCES ${RT}/update/check.cpp)
|
||||
|
||||
@@ -16,10 +16,17 @@
|
||||
// every one in the band exactly white (the career intro's movie
|
||||
// letterbox bled ~5% of the band into the strip's last row before);
|
||||
// - every fetch the anti-aliasing pass makes for a pixel reads only its
|
||||
// own span's texels.
|
||||
// own span's texels;
|
||||
// - FSR 1, where the picture is enlarged along the split axis: each of
|
||||
// EASU's taps for a screen pixel (texels floor(t - 0.5) - 1 to + 2 round
|
||||
// its centre t) reads only that pixel's span, and each neighbour RCAS
|
||||
// reads (the pixel's own picture, one pixel either way) lies in the same
|
||||
// span, so the black strips stay black and the band's edge rows equal its
|
||||
// middle ones.
|
||||
// The same checks run with planted faults (no split, a band edge one texel
|
||||
// off either way, clamping to the span's edges instead of its texel
|
||||
// centres): each must fail.
|
||||
// centres, an unclamped gather or EASU tap, RCAS reading across the edge):
|
||||
// each must fail.
|
||||
#include <gpu/edge_bars.h>
|
||||
#include <video/band_filter.h>
|
||||
#include <video/picture_fit.h>
|
||||
@@ -48,6 +55,8 @@ struct Model
|
||||
std::function<Span(float t, int32_t lo, int32_t hi, int32_t size)> spanOf = video::band::SpanOf;
|
||||
std::function<float(float t, Span s)> clamp = video::band::Clamp;
|
||||
std::function<int32_t(int32_t i, Span s)> clampTexel = video::band::ClampTexel;
|
||||
std::function<int32_t(int32_t i, int32_t j, int32_t lo, int32_t hi, int32_t outSize, int32_t srcSize)> rcasNeighbour =
|
||||
video::band::RcasNeighbour;
|
||||
};
|
||||
|
||||
// One axis of the frame image: texel values, the sampler's clamp to edge.
|
||||
@@ -174,13 +183,55 @@ static int AaCase(const Model& m, int32_t size, int32_t lo0, int32_t hi0)
|
||||
return g_failures - before;
|
||||
}
|
||||
|
||||
// FSR 1 along the split axis of one screen and image size, where the
|
||||
// picture is enlarged that way (else bicubic draws it: skipped). EASU's taps
|
||||
// for each screen pixel, and the screen pixels RCAS reads for it, against
|
||||
// where that pixel truly is. Returns the failures it added.
|
||||
static int FsrCase(const Model& m, const Shape& screen, const Shape& image)
|
||||
{
|
||||
const int before = g_failures;
|
||||
const video::PictureFit fit = video::FitPicture(screen.w, screen.h, 1280.0f, 720.0f, true, true);
|
||||
const video::FrameBand band = video::BandOf(image.w, image.h, fit.scaleX, fit.scaleY);
|
||||
const bool tall = fit.scaleY < 1.0f;
|
||||
const int32_t size = int32_t(tall ? image.h : image.w), pixels = tall ? fit.height : fit.width;
|
||||
if (pixels <= size)
|
||||
return 0;
|
||||
const int32_t trueLo = tall ? band.y0 : band.x0, trueHi = tall ? band.y1 : band.x1;
|
||||
int32_t lo = trueLo, hi = trueHi;
|
||||
m.band(lo, hi, size);
|
||||
auto own = [&](int32_t r) { return video::band::SpanOf(video::band::SourceOf(r, pixels, size), trueLo, trueHi, size); };
|
||||
for (int32_t r = 0; r < pixels && g_failures - before <= 8; r++)
|
||||
{
|
||||
const float t = video::band::SourceOf(r, pixels, size);
|
||||
const Span s = m.spanOf(t, lo, hi, size), truth = own(r);
|
||||
const int32_t fp = int32_t(std::floor(t - 0.5f));
|
||||
for (int32_t k = -1; k <= 2; k++)
|
||||
{
|
||||
const int32_t texel = std::clamp(m.clampTexel(fp + k, s), 0, size - 1); // and the sampler's clamp to edge
|
||||
CHECK(texel >= truth.a && texel < truth.b, "%ux%u, image %ux%u: EASU tap %+d of screen pixel %d (frame %.3f) reads texel %d outside its span [%d,%d)",
|
||||
screen.w, screen.h, image.w, image.h, k, r, t, texel, truth.a, truth.b);
|
||||
}
|
||||
for (int32_t d : { -1, 1 })
|
||||
{
|
||||
const int32_t read = m.rcasNeighbour(r, r + d, lo, hi, pixels, size);
|
||||
const Span rs = own(read);
|
||||
CHECK(rs.a == truth.a && rs.b == truth.b, "%ux%u, image %ux%u: RCAS's neighbour %+d of screen pixel %d reads pixel %d, in [%d,%d) not [%d,%d)",
|
||||
screen.w, screen.h, image.w, image.h, d, r, read, rs.a, rs.b, truth.a, truth.b);
|
||||
}
|
||||
}
|
||||
return g_failures - before;
|
||||
}
|
||||
|
||||
static int AllCases(const Model& m)
|
||||
{
|
||||
int failed = 0;
|
||||
for (const Shape& s : kScreens)
|
||||
for (const Shape& im : kImages)
|
||||
{
|
||||
for (int kind = 0; kind < 3; kind++)
|
||||
failed += ScaleCase(m, s, im, kind) ? 1 : 0;
|
||||
failed += FsrCase(m, s, im) ? 1 : 0;
|
||||
}
|
||||
// The anti-aliasing at the frame's resolution: 16:10, 4:3, 3:2 rows and
|
||||
// ultrawide columns at 1x and 2x.
|
||||
for (const Shape& s : { Shape{ 1280, 800 }, Shape{ 1024, 768 }, Shape{ 1200, 800 }, Shape{ 2560, 1080 } })
|
||||
@@ -281,7 +332,16 @@ int main()
|
||||
|
||||
const Model fixed{ "band_filter.h" };
|
||||
const int failed = AllCases(fixed);
|
||||
printf("scaling (3 filters x %zu screens x %zu images) and anti-aliasing fetches: %d case(s) failed\n", std::size(kScreens), std::size(kImages), failed);
|
||||
int fsrCases = 0;
|
||||
for (const Shape& s : kScreens)
|
||||
for (const Shape& im : kImages)
|
||||
{
|
||||
const video::PictureFit fit = video::FitPicture(s.w, s.h, 1280.0f, 720.0f, true, true);
|
||||
fsrCases += (fit.scaleY < 1.0f ? fit.height > int32_t(im.h) : fit.width > int32_t(im.w)) ? 1 : 0;
|
||||
}
|
||||
CHECK(fsrCases >= 8, "only %d FSR 1 cases enlarge along the split axis", fsrCases);
|
||||
printf("scaling (3 filters + FSR 1 where enlarged, %d cases, x %zu screens x %zu images) and anti-aliasing fetches: %d case(s) failed\n",
|
||||
fsrCases, std::size(kScreens), std::size(kImages), failed);
|
||||
|
||||
printf("planted faults (each must fail):\n");
|
||||
const Model planted[] = {
|
||||
@@ -293,6 +353,8 @@ int main()
|
||||
[](float t, Span s) { return std::clamp(t, float(s.a), float(s.b)); } },
|
||||
{ "no clamp of the gather", [](int32_t&, int32_t&, int32_t) {}, video::band::SpanOf, video::band::Clamp,
|
||||
[](int32_t i, Span) { return i; } },
|
||||
{ "RCAS reads across the edge", [](int32_t&, int32_t&, int32_t) {}, video::band::SpanOf, video::band::Clamp, video::band::ClampTexel,
|
||||
[](int32_t, int32_t j, int32_t, int32_t, int32_t outSize, int32_t) { return std::clamp(j, 0, outSize - 1); } },
|
||||
};
|
||||
for (const Model& m : planted)
|
||||
{
|
||||
|
||||
+185
-33
@@ -691,17 +691,21 @@ static void PlantedBackdrop(const char* what, const BackdropRule& wrong)
|
||||
}
|
||||
|
||||
// The backdrop's fill against pixel centres: every pixel of the scissor
|
||||
// past the band and outside the box is filled, exactly once; nothing in the
|
||||
// band or the box is (ties within 1/256 pixel skipped). `rects` makes the
|
||||
// rectangles (BackdropRects, or a planted fault). Returns the wrong pixels.
|
||||
// outside the box is filled, exactly once, if it lies past the band or in
|
||||
// a row above or below the box (the band's overscan rows, at every shape,
|
||||
// 16:9 too); nothing in the box is, nor in the band beside it within its
|
||||
// rows (ties within 1/256 pixel skipped). `rects` makes the rectangles
|
||||
// (BackdropRects, or a planted fault). Returns the wrong pixels; the
|
||||
// overscan pixels it checked (in the band, above or below the box) go to
|
||||
// `overscan`.
|
||||
using BackdropRectsFn = uint32_t (*)(const Box&, const float*, float, float, const Rect&, float, float, Rect*);
|
||||
static int BackdropOracle(const char* what, float sx, float sy, const Box& b, const float ps[4], float hostW, float hostH, const Rect& scissor,
|
||||
BackdropRectsFn rects, bool print = true)
|
||||
BackdropRectsFn rects, bool print = true, int* overscan = nullptr)
|
||||
{
|
||||
Rect r[6]{};
|
||||
Rect r[8]{};
|
||||
const uint32_t n = rects(b, ps, hostW, hostH, scissor, sx, sy, r);
|
||||
const float pxX = std::fabs(ps[0]) * hostW * 0.5f, pxY = std::fabs(ps[1]) * hostH * 0.5f;
|
||||
int wrong = 0, ties = 0;
|
||||
int wrong = 0, ties = 0, rows = 0;
|
||||
for (int32_t y = scissor.y0; y < scissor.y1; y++)
|
||||
for (int32_t x = scissor.x0; x < scissor.x1; x++)
|
||||
{
|
||||
@@ -719,23 +723,28 @@ static int BackdropOracle(const char* what, float sx, float sy, const Box& b, co
|
||||
// centres the first row's centre is past clip 1, still the band).
|
||||
const bool inBand = (sx >= 1.0f || std::fabs(cx) < sx) && (sy >= 1.0f || std::fabs(cy) < sy);
|
||||
const bool inBox = cx > b.x0 && cx < b.x1 && cy > b.y0 && cy < b.y1;
|
||||
const bool pastRows = cy > b.y1 || cy < b.y0; // above or below the box
|
||||
const bool want = !inBox && (!inBand || pastRows);
|
||||
rows += inBand && pastRows;
|
||||
int filled = 0;
|
||||
for (uint32_t i = 0; i < n; i++)
|
||||
filled += x >= r[i].x0 && x < r[i].x1 && y >= r[i].y0 && y < r[i].y1;
|
||||
if (filled != ((inBand || inBox) ? 0 : 1))
|
||||
if (filled != (want ? 1 : 0))
|
||||
{
|
||||
if (print && wrong < 3 && std::getenv("EDGE_BARS_DEBUG"))
|
||||
printf(" wrong px %d,%d: clip %.5f %.5f, band %d box %d filled %d\n", x, y, cx, cy, inBand, inBox, filled);
|
||||
printf(" wrong px %d,%d: clip %.5f %.5f, band %d box %d rows %d filled %d\n", x, y, cx, cy, inBand, inBox, pastRows, filled);
|
||||
wrong++;
|
||||
}
|
||||
}
|
||||
if (overscan)
|
||||
*overscan = rows;
|
||||
if (print)
|
||||
{
|
||||
printf("backdrop fill %s: %u rect(s)", what, n);
|
||||
for (uint32_t i = 0; i < n; i++)
|
||||
printf(" [%d,%d)x[%d,%d)", r[i].x0, r[i].x1, r[i].y0, r[i].y1);
|
||||
printf(", %d pixel(s) wrong (%d on an edge, skipped)\n", wrong, ties);
|
||||
CHECK(wrong == 0, "%s: %d pixels filled wrongly (in the band or the box, twice, or left)", what, wrong);
|
||||
printf(", %d overscan px, %d pixel(s) wrong (%d on an edge, skipped)\n", rows, wrong, ties);
|
||||
CHECK(wrong == 0, "%s: %d pixels filled wrongly (in the box or beside it in the band, twice, or left)", what, wrong);
|
||||
}
|
||||
return wrong;
|
||||
}
|
||||
@@ -801,7 +810,8 @@ static void BackdropTests()
|
||||
};
|
||||
const Case cases[] = { { "2560x1080", 0.75f, 1.0f }, { "2868x1320 (iPhone 18 Pro Max)", 0.818182f, 1.0f }, { "3440x1440", 0.744186f, 1.0f },
|
||||
{ "32:9", 0.5f, 1.0f }, { "1.95:1 (the picture past the screen edge)", 0.912f, 1.0f }, { "1280x800", 1.0f, 0.9f },
|
||||
{ "1024x768", 1.0f, 0.75f }, { "1200x800", 1.0f, 0.84375f } };
|
||||
{ "1024x768", 1.0f, 0.75f }, { "1200x800", 1.0f, 0.84375f }, { "1280x720 (16:9)", 1.0f, 1.0f },
|
||||
{ "2732x2048 (iPad Pro)", 1.0f, 0.750366f }, { "3024x1898 (MacBook Pro 14)", 1.0f, 0.896207f } };
|
||||
int wrong = 0;
|
||||
for (const Case& c : cases)
|
||||
{
|
||||
@@ -822,29 +832,66 @@ static void BackdropTests()
|
||||
wrong += BackdropOracle(what, c.sx, c.sy, results, ps, 1280, 2048, Rect{ 100, 10, 1180, 700 }, BackdropRects);
|
||||
}
|
||||
printf("backdrop fill: %d wrong pixel(s) over %zu shapes\n", wrong, std::size(cases));
|
||||
{
|
||||
// The overscan rows at the 8 screen shapes, 1x and 2x (half-pixel
|
||||
// centres): the results picture leaves rows of the band above and
|
||||
// below it at every shape, 16:9 too, and each of them is filled
|
||||
// once (the oracle above, which also counts them).
|
||||
printf("\nbackdrop overscan rows:\n");
|
||||
int rowsWrong = 0;
|
||||
for (const Shape& sh : kShapes)
|
||||
{
|
||||
const Box results{ -kBackdropX * sh.sx, kBackdropX * sh.sx, -kBackdropY * sh.sy, kBackdropY * sh.sy };
|
||||
char what[160];
|
||||
int px1 = 0, px2 = 0;
|
||||
snprintf(what, sizeof(what), "%s, overscan rows, 1x", sh.name);
|
||||
rowsWrong += BackdropOracle(what, sh.sx, sh.sy, results, ps, 1280, 2048, frame, BackdropRects, true, &px1);
|
||||
snprintf(what, sizeof(what), "%s, overscan rows, 2x, half-pixel", sh.name);
|
||||
rowsWrong += BackdropOracle(what, sh.sx, sh.sy, results, half, 2560, 4096, frame2, BackdropRects, true, &px2);
|
||||
// Rows: 0.019 of the band's half height each, ~7 * sy at 1x
|
||||
// (5 at 4:3), across the band's width.
|
||||
const int rowsMin = int(0.019f * sh.sy * 360.0f) - 1, wide = int(1280.0f * std::min(sh.sx, 1.0f)) - 2;
|
||||
CHECK(px1 >= 2 * rowsMin * wide && px2 >= 4 * 2 * rowsMin * wide, "%s: overscan px %d (1x) %d (2x), want >= %d and %d", sh.name, px1,
|
||||
px2, 2 * rowsMin * wide, 8 * rowsMin * wide);
|
||||
}
|
||||
printf("backdrop overscan rows: %d wrong pixel(s) over %zu shapes x 1x, 2x\n", rowsWrong, std::size(kShapes));
|
||||
}
|
||||
{
|
||||
// The rectangles themselves, 2868x1320 at 1x: columns past the
|
||||
// picture's sides (x +-1.104 band = 0.9033 clip: from column 1218,
|
||||
// to 62), and the corners past the band (0.8182 clip: column 1164)
|
||||
// above and below it (y +-0.981: rows 0..6 and 713..719).
|
||||
Rect r[6]{};
|
||||
// to 62), the corners past the band (0.8182 clip: column 1164)
|
||||
// above and below it (y +-0.981: rows 0..6 and 713..719), and the
|
||||
// overscan rows between them, inside the band (columns 116..1163).
|
||||
Rect r[8]{};
|
||||
const Box results{ -kBackdropX * 0.818182f, kBackdropX * 0.818182f, -kBackdropY, kBackdropY };
|
||||
const uint32_t n = BackdropRects(results, ps, 1280, 2048, frame, 0.818182f, 1.0f, r);
|
||||
const Rect want[6] = { { 1218, 0, 1280, 720 }, { 1164, 0, 1218, 7 }, { 1164, 713, 1218, 720 }, { 0, 0, 62, 720 }, { 62, 0, 116, 7 },
|
||||
{ 62, 713, 116, 720 } };
|
||||
CHECK(n == 6, "iPhone: %u rects, want 6", n);
|
||||
for (uint32_t i = 0; i < n && i < 6; i++)
|
||||
const Rect want[8] = { { 1218, 0, 1280, 720 }, { 1164, 0, 1218, 7 }, { 1164, 713, 1218, 720 }, { 0, 0, 62, 720 }, { 62, 0, 116, 7 },
|
||||
{ 62, 713, 116, 720 }, { 116, 0, 1164, 7 }, { 116, 713, 1164, 720 } };
|
||||
CHECK(n == 8, "iPhone: %u rects, want 8", n);
|
||||
for (uint32_t i = 0; i < n && i < 8; i++)
|
||||
CHECK(Same(r[i], want[i]), "iPhone: rect %u [%d,%d)x[%d,%d), want [%d,%d)x[%d,%d)", i, r[i].x0, r[i].x1, r[i].y0, r[i].y1, want[i].x0,
|
||||
want[i].x1, want[i].y0, want[i].y1);
|
||||
// Vert+: the rows above and below the band, whole (the picture stops
|
||||
// inside it in y and past the screen in x).
|
||||
// inside it in y and past the screen in x), then the band's overscan
|
||||
// rows (y +-0.981 x 0.75 = 0.736: rows 90..94 and 625..629).
|
||||
const Box r43{ -kBackdropX, kBackdropX, -kBackdropY * 0.75f, kBackdropY * 0.75f };
|
||||
const uint32_t n43 = BackdropRects(r43, ps, 1280, 2048, frame, 1.0f, 0.75f, r);
|
||||
CHECK(n43 == 2 && Same(r[0], Rect{ 0, 0, 1280, 90 }) && Same(r[1], Rect{ 0, 630, 1280, 720 }), "4:3: %u rects [%d,%d)x[%d,%d) ...", n43,
|
||||
r[0].x0, r[0].x1, r[0].y0, r[0].y1);
|
||||
// 16:9: nothing.
|
||||
CHECK(n43 == 4 && Same(r[0], Rect{ 0, 0, 1280, 90 }) && Same(r[1], Rect{ 0, 630, 1280, 720 }) && Same(r[2], Rect{ 0, 90, 1280, 95 }) &&
|
||||
Same(r[3], Rect{ 0, 625, 1280, 630 }),
|
||||
"4:3: %u rects [%d,%d)x[%d,%d) [%d,%d)x[%d,%d) ...", n43, r[0].x0, r[0].x1, r[0].y0, r[0].y1, r[2].x0, r[2].x1, r[2].y0, r[2].y1);
|
||||
// 16:9: the overscan rows alone, rows 0..6 and 713..719, the whole
|
||||
// width; none for a picture over the whole band.
|
||||
const Box r169{ -kBackdropX, kBackdropX, -kBackdropY, kBackdropY };
|
||||
CHECK(BackdropRects(r169, ps, 1280, 2048, frame, 1.0f, 1.0f, r) == 0, "16:9: a backdrop fills nothing");
|
||||
const uint32_t n169 = BackdropRects(r169, ps, 1280, 2048, frame, 1.0f, 1.0f, r);
|
||||
CHECK(n169 == 2 && Same(r[0], Rect{ 0, 0, 1280, 7 }) && Same(r[1], Rect{ 0, 713, 1280, 720 }), "16:9: %u rects [%d,%d)x[%d,%d) ...", n169,
|
||||
r[0].x0, r[0].x1, r[0].y0, r[0].y1);
|
||||
const Box whole{ -1.0f, 1.0f, -1.0f, 1.0f };
|
||||
CHECK(BackdropRects(whole, ps, 1280, 2048, frame, 1.0f, 1.0f, r) == 0, "16:9: a picture over the whole band fills nothing at 1x");
|
||||
// ... and at 2x with half-pixel centres only the top row, whose
|
||||
// centre lies past clip 1 (the picture is one host row lower: its
|
||||
// bottom edge is past the frame's).
|
||||
const uint32_t n2 = BackdropRects(whole, half, 2560, 4096, frame2, 1.0f, 1.0f, r);
|
||||
CHECK(n2 == 1 && Same(r[0], Rect{ 0, 0, 2560, 1 }), "16:9 2x: %u rects [%d,%d)x[%d,%d) ...", n2, r[0].x0, r[0].x1, r[0].y0, r[0].y1);
|
||||
}
|
||||
printf("\nplanted, backdrop fill (each must fail):\n");
|
||||
{
|
||||
@@ -863,7 +910,7 @@ static void BackdropTests()
|
||||
return BlackoutRects(ps, w, h, sc, sx, sy, out);
|
||||
});
|
||||
run("past the picture only (no corners)", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy, Rect* out) {
|
||||
Rect r[6]{};
|
||||
Rect r[8]{};
|
||||
const uint32_t n = BackdropRects(b, ps, w, h, sc, sx, sy, r);
|
||||
uint32_t m = 0;
|
||||
for (uint32_t i = 0; i < n; i++)
|
||||
@@ -877,6 +924,69 @@ static void BackdropTests()
|
||||
wider.x1 = std::min(b.x1, sx);
|
||||
return BackdropRects(wider, ps, w, h, sc, sx, sy, out);
|
||||
});
|
||||
// The overscan rows, planted: each must fail. runOverscan checks
|
||||
// every shape at 1x and 2x; with `every`, each shape must fail (16:9
|
||||
// included), else at least one (a fault only some shapes have).
|
||||
auto runOverscan = [&](const char* what, BackdropRectsFn f, bool every) {
|
||||
int bad = 0, shapesFailing = 0;
|
||||
for (const Case& c : cases)
|
||||
{
|
||||
const Box results{ -kBackdropX * c.sx, kBackdropX * c.sx, -kBackdropY * c.sy, kBackdropY * c.sy };
|
||||
const int here = BackdropOracle(what, c.sx, c.sy, results, ps, 1280, 2048, frame, f, false) +
|
||||
BackdropOracle(what, c.sx, c.sy, results, half, 2560, 4096, frame2, f, false);
|
||||
bad += here;
|
||||
shapesFailing += here > 0;
|
||||
}
|
||||
printf("planted %s: %d wrong pixel(s), %d of %zu shapes\n", what, bad, shapesFailing, std::size(cases));
|
||||
CHECK(every ? shapesFailing == int(std::size(cases)) : bad > 0, "planted %s: %d of %zu shapes failed", what, shapesFailing,
|
||||
std::size(cases));
|
||||
};
|
||||
// 811cdf2's fill: past the band only, the overscan rows left.
|
||||
runOverscan("no overscan rows (past the band only, as 811cdf2)", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx,
|
||||
float sy, Rect* out) { return PastBandRects(b, ps, w, h, sc, sx, sy, out); }, true);
|
||||
// Rows only where something lies past the band: 16:9 left.
|
||||
runOverscan("overscan rows only on wide and tall screens", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx,
|
||||
float sy, Rect* out) {
|
||||
const uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
|
||||
return n + (sx < 1.0f || sy < 1.0f ? OverscanRows(b, ps, w, h, sc, sx, sy, out + n) : 0u);
|
||||
}, false);
|
||||
// Rows to clip 1 where the band's edge is the screen's (no
|
||||
// kFillEdge): at 2x the outermost row and column are left.
|
||||
runOverscan("overscan rows to clip 1, not kFillEdge", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy,
|
||||
Rect* out) {
|
||||
uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
|
||||
const float bx = std::min(sx, 1.0f), by = std::min(sy, 1.0f);
|
||||
for (const Rect& r : { ClipToHost(-bx, bx, b.y1, by, ps, w, h), ClipToHost(-bx, bx, -by, b.y0, ps, w, h) })
|
||||
if (const Rect c = Intersect(r, sc); !Empty(c))
|
||||
out[n++] = c;
|
||||
return n;
|
||||
}, false);
|
||||
// Rows across the whole screen under Hor+: twice where the fill
|
||||
// past the band has them already.
|
||||
runOverscan("overscan rows across the whole screen", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy,
|
||||
Rect* out) {
|
||||
const uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
|
||||
return n + OverscanRows(b, ps, w, h, sc, 1.0f, sy, out + n);
|
||||
}, false);
|
||||
// Rows from just inside the picture: its own top and bottom rows
|
||||
// blacked.
|
||||
runOverscan("overscan rows from inside the picture", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy,
|
||||
Rect* out) {
|
||||
Box in = b;
|
||||
in.y0 += 0.01f;
|
||||
in.y1 -= 0.01f;
|
||||
const uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
|
||||
return n + OverscanRows(in, ps, w, h, sc, sx, sy, out + n);
|
||||
}, true);
|
||||
// Rows short of the picture: a row of the race left above it.
|
||||
runOverscan("overscan rows short of the picture", [](const Box& b, const float* ps, float w, float h, const Rect& sc, float sx, float sy,
|
||||
Rect* out) {
|
||||
Box out_ = b;
|
||||
out_.y0 -= 0.01f;
|
||||
out_.y1 += 0.01f;
|
||||
const uint32_t n = PastBandRects(b, ps, w, h, sc, sx, sy, out);
|
||||
return n + OverscanRows(out_, ps, w, h, sc, sx, sy, out + n);
|
||||
}, true);
|
||||
}
|
||||
|
||||
printf("\nbackdrop plan (the draw as the renderer hands it over):\n");
|
||||
@@ -884,15 +994,57 @@ static void BackdropTests()
|
||||
const Matrix iphone = FrontEndMatrix(0.818182f, 1.0f), deck = FrontEndMatrix(1.0f, 0.9f), m169 = FrontEndMatrix(1.0f, 1.0f);
|
||||
const EPolys results = Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 0.818182f, 1.0f, kBackdropColour), iphone);
|
||||
const Draw d = BackdropDraw(iphone, 0.818182f, 1.0f, ps);
|
||||
const Rect want[6] = { { 1218, 0, 1280, 720 }, { 1164, 0, 1218, 7 }, { 1164, 713, 1218, 720 }, { 0, 0, 62, 720 }, { 62, 0, 116, 7 },
|
||||
{ 62, 713, 116, 720 } };
|
||||
BackdropPlanCase("iPhone results", d, results, Fill::Backdrop, 6, want);
|
||||
const Rect deckWant[2] = { { 0, 0, 1280, 36 }, { 0, 684, 1280, 720 } };
|
||||
const Rect want[8] = { { 1218, 0, 1280, 720 }, { 1164, 0, 1218, 7 }, { 1164, 713, 1218, 720 }, { 0, 0, 62, 720 }, { 62, 0, 116, 7 },
|
||||
{ 62, 713, 116, 720 }, { 116, 0, 1164, 7 }, { 116, 713, 1164, 720 } };
|
||||
BackdropPlanCase("iPhone results", d, results, Fill::Backdrop, 8, want);
|
||||
// The Deck (1280x800): the rows past the band (0..35, 684..719),
|
||||
// then its overscan rows (y 0.981 x 0.9 = 0.883: 36..41, 678..683).
|
||||
const Rect deckWant[4] = { { 0, 0, 1280, 36 }, { 0, 684, 1280, 720 }, { 0, 36, 1280, 42 }, { 0, 678, 1280, 684 } };
|
||||
BackdropPlanCase("Deck results", BackdropDraw(deck, 1.0f, 0.9f, ps),
|
||||
Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 1.0f, 0.9f, kBackdropColour), deck), Fill::Backdrop, 2, deckWant);
|
||||
// Never at 16:9 (nothing past the band to fill).
|
||||
BackdropPlanCase("16:9 results", BackdropDraw(m169, 1.0f, 1.0f, ps),
|
||||
Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 1.0f, 1.0f, kBackdropColour), m169), Fill::None);
|
||||
Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 1.0f, 0.9f, kBackdropColour), deck), Fill::Backdrop, 4, deckWant);
|
||||
// 16:9 too: its overscan rows alone (rows 0..6 and 713..719), also
|
||||
// from a ppc/ without the Vert+ hooks (no front-end y scale).
|
||||
const Draw d169 = BackdropDraw(m169, 1.0f, 1.0f, ps);
|
||||
const EPolys results169 = Polys(Rectangle(-kBackdropX, kBackdropX, -kBackdropY, kBackdropY, 1.0f, 1.0f, kBackdropColour), m169);
|
||||
const Rect want169[2] = { { 0, 0, 1280, 7 }, { 0, 713, 1280, 720 } };
|
||||
BackdropPlanCase("16:9 results", d169, results169, Fill::Backdrop, 2, want169);
|
||||
Draw old169 = d169;
|
||||
old169.frontEndY = 0.0f;
|
||||
BackdropPlanCase("16:9 results, no Vert+ hooks", old169, results169, Fill::Backdrop, 2, want169);
|
||||
// ... and at 2x with half-pixel centres: the picture one host row
|
||||
// lower (its top at 2 x 6.84 + 1 = 14.7, its bottom at 1427.3):
|
||||
// rows 0..14 and 1427..1439.
|
||||
float half169[4];
|
||||
FrontEndPs(half169, 640.5f, 360.5f);
|
||||
const Rect want169x2[2] = { { 0, 0, 2560, 15 }, { 0, 1427, 2560, 1440 } };
|
||||
BackdropPlanCase("16:9 results, 2x", BackdropDraw(m169, 1.0f, 1.0f, half169, 2560.0f, 4096.0f, Rect{ 0, 0, 2560, 1440 }), results169,
|
||||
Fill::Backdrop, 2, want169x2);
|
||||
// Every register check refuses at 16:9 as elsewhere (a fade, a dim
|
||||
// or the HUD never gets this far: MatchBackdrop's cases above run
|
||||
// at 16:9 too).
|
||||
auto refuse169 = [&](const char* what, auto change) {
|
||||
Draw x = d169;
|
||||
Matrix m = m169;
|
||||
change(x, m);
|
||||
x.c = m.c;
|
||||
char name[96];
|
||||
snprintf(name, sizeof(name), "16:9, %s", what);
|
||||
BackdropPlanCase(name, x, results169, Fill::None);
|
||||
};
|
||||
refuse169("taken by another rule", [](Draw& x, Matrix&) { x.leftAlone = false; });
|
||||
refuse169("not the front-end shader", [](Draw& x, Matrix&) { x.frontEndShader = false; });
|
||||
refuse169("inside an occlusion query", [](Draw& x, Matrix&) { x.occlusion = true; });
|
||||
refuse169("two quads", [](Draw& x, Matrix&) { x.count = 8; });
|
||||
refuse169("additive blend", [](Draw& x, Matrix&) { x.blend0 = 0x01060106; });
|
||||
refuse169("t0 not sampled", [](Draw& x, Matrix&) { x.t0Sampled = false; });
|
||||
refuse169("t0 32x32 (the white texture)", [](Draw& x, Matrix&) { x.t0Width = x.t0Height = 32; });
|
||||
refuse169("x translation c3", [](Draw&, Matrix& m) { m.c[3] = 0.01f; });
|
||||
refuse169("c5 not the front end's y scale", [](Draw& x, Matrix&) { x.frontEndY = 0.83f; });
|
||||
refuse169("VTE x offset off", [](Draw& x, Matrix&) { x.vte = 0x43D; });
|
||||
EPolys faded169 = results169;
|
||||
for (auto& w : faded169.w)
|
||||
w[3] = 0xB87F7F7F;
|
||||
BackdropPlanCase("16:9, alpha B8 (a fade-in)", d169, faded169, Fill::None);
|
||||
// A flat full-band layer over the 32x32 white texture is not a
|
||||
// picture (the black frame's and the fades' kind).
|
||||
Draw flat = d;
|
||||
@@ -922,7 +1074,7 @@ static void BackdropTests()
|
||||
refuse("VTE y offset off", [](Draw& x, Matrix&) { x.vte = 0x437; });
|
||||
Draw opaque = d;
|
||||
opaque.blend0 = 0x00010001;
|
||||
BackdropPlanCase("blending off", opaque, results, Fill::Backdrop, 6, want);
|
||||
BackdropPlanCase("blending off", opaque, results, Fill::Backdrop, 8, want);
|
||||
// PlanFill (bars and black frames) never takes it: the renderer asks
|
||||
// PlanBackdrop only when PlanFill found nothing.
|
||||
CHECK(RunPlan(d, results).fill == Fill::None, "PlanFill took the backdrop");
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// SpeedBreaker tests. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Settings > Graphics > Upscaling's FSR 1: which pass draws a frame
|
||||
// (video/upscale.h, on video::FitPicture's pictures), Sharpening as RCAS's
|
||||
// strength, the shaders' constants against AMD's formulas, and their GLSL
|
||||
// (video/fsr1.cpp): AMD's two headers compiled in byte for byte, one main
|
||||
// in RCAS's (the tone variant renames it), and the band rules of
|
||||
// video/band_filter.h in their code.
|
||||
// Arguments: runtime/thirdparty/fsr1 (the vendored headers), runtime/video
|
||||
// (presenter.cpp and fsr1.cpp read, and ../user/settings.cpp: FSR 1 isn't
|
||||
// offered on iPhone and iPad, and no FSR 1 shader is compiled unless a
|
||||
// frame's pass is FSR 1), and a folder the GLSL is written to
|
||||
// (fsr1_glsl_check, when glslangValidator is installed, compiles it).
|
||||
//
|
||||
// Build by hand (from the repo root, after a CMake configure has written
|
||||
// build/main/runtime/generated/fsr1_headers.inc):
|
||||
// c++ -std=c++20 -O2 -UNDEBUG -I runtime -I build/main/runtime/generated tests/fsr1_test.cpp \
|
||||
// runtime/video/fsr1.cpp runtime/video/picture_fit.cpp -o build/fsr1_test
|
||||
// build/fsr1_test runtime/thirdparty/fsr1 runtime/video build/fsr1_glsl
|
||||
#include <user/settings.h>
|
||||
#include <video/fsr1.h>
|
||||
#include <video/picture_fit.h>
|
||||
#include <video/upscale.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
int s_failures = 0;
|
||||
|
||||
#define CHECK(cond, ...) \
|
||||
do \
|
||||
{ \
|
||||
if (!(cond)) \
|
||||
{ \
|
||||
s_failures++; \
|
||||
fprintf(stderr, "FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fprintf(stderr, "\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
using video::upscale::Choose;
|
||||
using video::upscale::Mode;
|
||||
using video::upscale::Pass;
|
||||
|
||||
std::string Read(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
std::stringstream text;
|
||||
text << in.rdbuf();
|
||||
return text.str();
|
||||
}
|
||||
|
||||
float Float(uint32_t bits)
|
||||
{
|
||||
float f;
|
||||
memcpy(&f, &bits, 4);
|
||||
return f;
|
||||
}
|
||||
|
||||
size_t Count(const std::string& text, const std::string& what)
|
||||
{
|
||||
size_t n = 0;
|
||||
for (size_t at = text.find(what); at != std::string::npos; at = text.find(what, at + 1))
|
||||
n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
// The pass for a 1280x720-shaped frame of imageW x imageH on a screen,
|
||||
// where the presenter puts it (Aspect Ratio Auto, the hooks present).
|
||||
Pass PassOn(Mode mode, uint32_t imageW, uint32_t imageH, uint32_t screenW, uint32_t screenH, int32_t* dw = nullptr, int32_t* dh = nullptr)
|
||||
{
|
||||
const video::PictureFit fit = video::FitPicture(screenW, screenH, 1280.0f, 720.0f, true, true);
|
||||
if (dw)
|
||||
*dw = fit.width, *dh = fit.height;
|
||||
return Choose(int32_t(mode), imageW, imageH, fit.width, fit.height);
|
||||
}
|
||||
|
||||
void PassSelection()
|
||||
{
|
||||
static_assert(int32_t(Mode::Bicubic) == int32_t(settings::ScalingMode::Bicubic));
|
||||
static_assert(int32_t(Mode::Linear) == int32_t(settings::ScalingMode::Linear));
|
||||
static_assert(int32_t(Mode::Fsr1) == int32_t(settings::ScalingMode::Fsr1));
|
||||
struct Case
|
||||
{
|
||||
const char* what;
|
||||
uint32_t imageW, imageH, screenW, screenH;
|
||||
Pass fsr; // with Upscaling on FSR 1
|
||||
};
|
||||
const Case cases[] = {
|
||||
{ "1x on 1080p", 1280, 720, 1920, 1080, Pass::Fsr1 },
|
||||
{ "1x on 1440p", 1280, 720, 2560, 1440, Pass::Fsr1 },
|
||||
{ "1x on a MacBook Pro 14 (Vert+)", 1280, 720, 3024, 1898, Pass::Fsr1 },
|
||||
{ "1.5x on 4K", 1920, 1080, 3840, 2160, Pass::Fsr1 },
|
||||
{ "1x on the Steam Deck (720 rows stretched to 800)", 1280, 720, 1280, 800, Pass::Fsr1 },
|
||||
{ "2x on a MacBook Pro 14 (Auto on an M1 Pro)", 2560, 1440, 3024, 1898, Pass::Fsr1 },
|
||||
{ "1x on an ultrawide (Hor+)", 1280, 720, 3440, 1440, Pass::Fsr1 },
|
||||
{ "1x in a 1280x720 window (1:1)", 1280, 720, 1280, 720, Pass::Bicubic },
|
||||
{ "2x on 1080p (shrunk)", 2560, 1440, 1920, 1080, Pass::Bicubic },
|
||||
{ "2x on 1440p (1:1)", 2560, 1440, 2560, 1440, Pass::Bicubic },
|
||||
{ "3x2 on an ultrawide (columns shrunk)", 3840, 1440, 3440, 1440, Pass::Bicubic },
|
||||
{ "1x in a small window (shrunk)", 1280, 720, 960, 540, Pass::Bicubic },
|
||||
{ "1x2 on 1080p (columns enlarged, rows shrunk)", 1280, 1440, 1920, 1080, Pass::Bicubic },
|
||||
};
|
||||
for (const Case& c : cases)
|
||||
{
|
||||
int32_t dw = 0, dh = 0;
|
||||
const Pass fsr = PassOn(Mode::Fsr1, c.imageW, c.imageH, c.screenW, c.screenH, &dw, &dh);
|
||||
CHECK(fsr == c.fsr, "%s (%ux%u -> %dx%d): FSR 1 setting gives %s, want %s", c.what, c.imageW, c.imageH, dw, dh,
|
||||
video::upscale::PassName(fsr), video::upscale::PassName(c.fsr));
|
||||
// The other settings draw as they always did, enlarged or not.
|
||||
CHECK(PassOn(Mode::Bicubic, c.imageW, c.imageH, c.screenW, c.screenH) == Pass::Bicubic, "%s: bicubic", c.what);
|
||||
CHECK(PassOn(Mode::Linear, c.imageW, c.imageH, c.screenW, c.screenH) == Pass::Linear, "%s: bilinear", c.what);
|
||||
printf(" %-50s %4ux%-4u -> %4dx%-4d %s\n", c.what, c.imageW, c.imageH, dw, dh, video::upscale::PassName(fsr));
|
||||
}
|
||||
// No picture, or a value the menu never stores: bicubic.
|
||||
CHECK(Choose(int32_t(Mode::Fsr1), 0, 0, 1920, 1080) == Pass::Bicubic, "no frame");
|
||||
CHECK(Choose(int32_t(Mode::Fsr1), 1280, 720, 0, 0) == Pass::Bicubic, "no picture");
|
||||
CHECK(Choose(7, 1280, 720, 1920, 1080) == Pass::Bicubic, "an unknown mode");
|
||||
// Enlarged along one axis by a single pixel counts; along neither doesn't.
|
||||
CHECK(video::upscale::Enlarged(1280, 720, 1281, 720) && !video::upscale::Enlarged(1280, 720, 1280, 720), "one pixel");
|
||||
}
|
||||
|
||||
void Sharpness()
|
||||
{
|
||||
using video::upscale::RcasAmount;
|
||||
using video::upscale::RcasStops;
|
||||
CHECK(RcasAmount(0.0f) == 0.0f && std::isinf(RcasStops(0.0f)), "0 turns RCAS off");
|
||||
CHECK(RcasAmount(-1.0f) == 0.0f && RcasAmount(NAN) == 0.0f, "below 0, NaN: off");
|
||||
CHECK(RcasAmount(1.0f) == 1.0f && RcasStops(1.0f) == 0.0f, "1 is RCAS's maximum (0 stops)");
|
||||
CHECK(RcasAmount(2.0f) == 1.0f, "above 1: the maximum");
|
||||
CHECK(std::fabs(RcasStops(0.5f) - 1.0f) < 1e-6f, "0.5: one stop below");
|
||||
float last = -1.0f;
|
||||
for (int i = 0; i <= 20; i++)
|
||||
{
|
||||
const float a = RcasAmount(float(i) / 20.0f);
|
||||
CHECK(a > last || (i == 0 && a == 0.0f), "monotonic at %d/20", i);
|
||||
last = a;
|
||||
}
|
||||
// What RCAS gets: FsrRcasCon's con.x is the linear factor 2^-stops.
|
||||
for (float s : { 0.05f, 0.3f, 0.5f, 1.0f })
|
||||
{
|
||||
const auto con = video::fsr1::RcasConstants(RcasStops(s));
|
||||
CHECK(std::fabs(Float(con[0]) - RcasAmount(s)) < 1e-5f, "Sharpening %.2f: con.x %.6f, want %.6f", s, Float(con[0]), RcasAmount(s));
|
||||
}
|
||||
const auto off = video::fsr1::RcasConstants(RcasStops(0.0f));
|
||||
CHECK(off[0] == 0 && off[1] == 0, "off: con 0 (the shader copies EASU's picture)");
|
||||
}
|
||||
|
||||
void EasuConstants()
|
||||
{
|
||||
// FsrEasuCon's formulas (ffx_fsr1.h), for the image's whole size.
|
||||
const std::array<uint32_t, 4> sizes[] = { { 1280, 720, 1920, 1080 }, { 1280, 720, 1280, 800 }, { 1920, 1080, 3840, 2160 },
|
||||
{ 1280, 720, 3024, 1898 } };
|
||||
for (auto [sw, sh, dw, dh] : sizes)
|
||||
{
|
||||
const auto c = video::fsr1::EasuConstants(sw, sh, dw, dh);
|
||||
const float x = float(sw) / float(dw), y = float(sh) / float(dh);
|
||||
CHECK(std::fabs(Float(c[0]) - x) < 1e-6f && std::fabs(Float(c[1]) - y) < 1e-6f, "%ux%u -> %ux%u: con0.xy", sw, sh, dw, dh);
|
||||
CHECK(std::fabs(Float(c[2]) - (0.5f * x - 0.5f)) < 1e-6f && std::fabs(Float(c[3]) - (0.5f * y - 0.5f)) < 1e-6f, "con0.zw");
|
||||
CHECK(Float(c[4]) == 1.0f / float(sw) && Float(c[5]) == 1.0f / float(sh), "con1.xy");
|
||||
CHECK(Float(c[6]) == 1.0f / float(sw) && Float(c[7]) == -1.0f / float(sh), "con1.zw");
|
||||
CHECK(Float(c[8]) == -1.0f / float(sw) && Float(c[9]) == 2.0f / float(sh), "con2.xy");
|
||||
CHECK(Float(c[10]) == 1.0f / float(sw) && Float(c[11]) == 2.0f / float(sh), "con2.zw");
|
||||
CHECK(Float(c[12]) == 0.0f && Float(c[13]) == 4.0f / float(sh) && c[14] == 0 && c[15] == 0, "con3");
|
||||
}
|
||||
}
|
||||
|
||||
void Glsl(const std::filesystem::path& fsrDir, const std::filesystem::path& videoDir, const std::filesystem::path& out)
|
||||
{
|
||||
const std::string easu = video::fsr1::EasuGlsl(), rcas = video::fsr1::RcasGlsl();
|
||||
// AMD's headers inside, byte for byte as vendored (unmodified).
|
||||
size_t halfInHeaders = 0;
|
||||
for (const char* name : { "ffx_a.h", "ffx_fsr1.h" })
|
||||
{
|
||||
const std::string header = Read(fsrDir / name);
|
||||
CHECK(header.size() > 10000, "%s read (%zu bytes)", name, header.size());
|
||||
CHECK(easu.find(header) != std::string::npos && rcas.find(header) != std::string::npos, "%s compiled in unchanged", name);
|
||||
halfInHeaders += Count(header, "#define A_HALF"); // their usage examples, in comments
|
||||
}
|
||||
CHECK(Count(easu, "#define FSR_EASU_F 1") == 1 && Count(rcas, "#define FSR_RCAS_F 1") == 1, "the 32-bit paths");
|
||||
CHECK(Count(easu, "#define A_HALF") == halfInHeaders && Count(rcas, "#define A_HALF") == halfInHeaders,
|
||||
"no 16-bit path of ours (MoltenVK, Intel): %zu, %zu, %zu in the headers' comments", Count(easu, "#define A_HALF"),
|
||||
Count(rcas, "#define A_HALF"), halfInHeaders);
|
||||
CHECK(Count(rcas, "void main()") == 1, "RCAS has one main (the tone variant renames it)");
|
||||
// band_filter.h's rules, as the GLSL writes them.
|
||||
CHECK(easu.find("vec2 pos = (vec2(ip) + 0.5) * vec2(p.srcSize) / vec2(p.outSize);") != std::string::npos, "EASU: SourceOf");
|
||||
CHECK(easu.find("return t < float(lo) ? ivec2(0, lo) : t < float(hi) ? ivec2(lo, hi) : ivec2(hi, size);") != std::string::npos, "EASU: SpanOf");
|
||||
CHECK(easu.find("texelFetch(src, clamp(t, spanLo, spanHi), 0)") != std::string::npos, "EASU: ClampTexel");
|
||||
CHECK(easu.find("p.bandOn == 0u ? textureGather(src, q, 0) : quadIn(q, 0)") != std::string::npos, "EASU: gathers without a band");
|
||||
CHECK(rcas.find("float t = (float(i) + 0.5) * float(srcSize) / float(outSize);") != std::string::npos, "RCAS: SourceOf");
|
||||
CHECK(rcas.find("return t < float(lo) ? 0 : t < float(hi) ? 1 : 2;") != std::string::npos, "RCAS: SpanIndex");
|
||||
CHECK(Count(rcas, "!= centreSpan.") == 2 && rcas.find("t.x = centre.x;") != std::string::npos && rcas.find("t.y = centre.y;") != std::string::npos,
|
||||
"RCAS: RcasNeighbour on both axes");
|
||||
// The presenter: the pass from Choose, RCAS's tone variant from ToneVariant, EASU before the swapchain's pass.
|
||||
const std::string presenter = Read(videoDir / "presenter.cpp");
|
||||
CHECK(presenter.find("upscale::Choose(settings::GetInt(settings::Id::Scaling), w, h, dw, dh)") != std::string::npos, "ChoosePass");
|
||||
CHECK(presenter.find("ToneVariant(fsr1::RcasGlsl().c_str())") != std::string::npos, "RCAS's tone variant");
|
||||
const size_t easuAt = presenter.find("RecordEasu(cmd, scaleInput, w, h, dw, dh, band, bandOn);");
|
||||
const size_t passAt = presenter.find("color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // black bars");
|
||||
CHECK(easuAt != std::string::npos && passAt != std::string::npos && easuAt < passAt, "EASU before the swapchain image's pass");
|
||||
// No FSR 1 shader or pipeline unless a frame's pass is FSR 1: the
|
||||
// GLSL is compiled only in EnsureFsr and RcasPipeline, and every
|
||||
// EnsureFsr() call is behind "pass == upscale::Pass::Fsr1".
|
||||
const size_t ensureAt = presenter.find("bool EnsureFsr()"), imageAt = presenter.find("void EnsureFsrImage(");
|
||||
CHECK(ensureAt != std::string::npos && imageAt != std::string::npos && ensureAt < imageAt, "EnsureFsr, RcasPipeline, EnsureFsrImage in order");
|
||||
size_t glslAt = 0, glslCount = 0;
|
||||
while ((glslAt = presenter.find("fsr1::", glslAt)) != std::string::npos)
|
||||
{
|
||||
if (presenter.compare(glslAt, 15, "fsr1::EasuGlsl(") == 0 || presenter.compare(glslAt, 15, "fsr1::RcasGlsl(") == 0)
|
||||
{
|
||||
glslCount++;
|
||||
CHECK(glslAt > ensureAt && glslAt < imageAt, "FSR 1 GLSL compiled outside EnsureFsr/RcasPipeline (offset %zu)", glslAt);
|
||||
}
|
||||
glslAt += 6;
|
||||
}
|
||||
CHECK(glslCount == 3, "FSR 1 GLSL compiled in 3 places (EASU, RCAS, RCAS's tone variant): %zu", glslCount);
|
||||
size_t calls = 0;
|
||||
for (size_t callAt = presenter.find("EnsureFsr()"); callAt != std::string::npos; callAt = presenter.find("EnsureFsr()", callAt + 11))
|
||||
{
|
||||
if (callAt == ensureAt + 5)
|
||||
continue; // the definition
|
||||
calls++;
|
||||
const size_t lineAt = presenter.rfind('\n', callAt) + 1;
|
||||
CHECK(presenter.substr(lineAt, callAt - lineAt).find("pass == upscale::Pass::Fsr1 && ") != std::string::npos,
|
||||
"EnsureFsr() called without the FSR 1 pass: %s", presenter.substr(lineAt, presenter.find('\n', callAt) - lineAt).c_str());
|
||||
}
|
||||
CHECK(calls == 2, "EnsureFsr() called from ChoosePass and NFSMW_UPSCALE_COMPARE only: %zu", calls);
|
||||
// iPhone and iPad: no FSR 1 in the menu (user/settings.cpp: its entry
|
||||
// compiled out there, so "fsr1" reads as unknown = bicubic).
|
||||
const std::string settingsCpp = Read(videoDir / ".." / "user" / "settings.cpp");
|
||||
const size_t entryAt = settingsCpp.find("{ int32_t(ScalingMode::Fsr1), \"fsr1\", \"FSR 1\" },");
|
||||
const size_t ifAt = settingsCpp.rfind("#if !(defined(__APPLE__) && TARGET_OS_IOS)", entryAt);
|
||||
const size_t endifAt = settingsCpp.find("#endif", ifAt == std::string::npos ? 0 : ifAt);
|
||||
CHECK(entryAt != std::string::npos && ifAt != std::string::npos && endifAt > entryAt
|
||||
&& settingsCpp.substr(ifAt, entryAt - ifAt).find("#endif") == std::string::npos,
|
||||
"the FSR 1 Upscaling entry is compiled out on iPhone and iPad");
|
||||
// For fsr1_glsl_check: the shaders as glslang gets them (the tone
|
||||
// variant as ToneVariant makes it).
|
||||
std::filesystem::create_directories(out);
|
||||
std::ofstream(out / "easu.comp") << easu;
|
||||
std::ofstream(out / "rcas.frag") << rcas;
|
||||
std::string tone = rcas;
|
||||
tone.replace(tone.find("void main()"), 11, "void scaleMain()");
|
||||
tone += "\nlayout(set = 0, binding = 1) uniform sampler2D toneTable;\nvoid main()\n{\n scaleMain();\n"
|
||||
" color = vec4(textureLod(toneTable, vec2(color.r, 0.5), 0.0).r, color.gb, color.a);\n}\n";
|
||||
std::ofstream(out / "rcas_tone.frag") << tone;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
printf("the pass with Upscaling on FSR 1:\n");
|
||||
PassSelection();
|
||||
Sharpness();
|
||||
EasuConstants();
|
||||
if (argc >= 4)
|
||||
Glsl(argv[1], argv[2], argv[3]);
|
||||
else
|
||||
printf("(no folders given: the GLSL checks skipped)\n");
|
||||
if (s_failures)
|
||||
{
|
||||
printf("%d check(s) FAILED\n", s_failures);
|
||||
return 1;
|
||||
}
|
||||
printf("all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Tests for hid/key_map.h: the 24 keyboard-driven controls, their two keys each, and how held keys
|
||||
// become buttons, triggers and stick axes. Header only; from the repo root:
|
||||
// clang++ -std=c++20 -O2 -Iruntime tests/key_map_test.cpp -o build/key_map_test && build/key_map_test
|
||||
#include <hid/key_map.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#include <set>
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
using namespace hid::keymap;
|
||||
|
||||
static Resolved Press(std::initializer_list<Control> held)
|
||||
{
|
||||
bool pressed[kControls] = {};
|
||||
for (Control c : held)
|
||||
pressed[size_t(c)] = true;
|
||||
return Resolve(pressed);
|
||||
}
|
||||
|
||||
static bool NoAxis(const Resolved& r)
|
||||
{
|
||||
for (size_t a = 0; a < size_t(Axis::Count); a++)
|
||||
if (r.axisSet[a] || r.axis[a] != 0.0f)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
// The table: every control once, in order, each with its own setting
|
||||
// among the Key options.
|
||||
CHECK(kControls == 24, "%zu controls", kControls);
|
||||
std::set<settings::Id> ids;
|
||||
for (size_t i = 0; i < kControls; i++)
|
||||
{
|
||||
CHECK(size_t(kEntries[i].control) == i, "entry %zu is its control", i);
|
||||
CHECK(ids.insert(kEntries[i].id).second && ids.insert(kEntries[i].id2).second, "entry %zu: its own two settings", i);
|
||||
CHECK(kEntries[i].id == settings::Id(size_t(settings::Id::KeyRightTrigger) + 2 * i), "entry %zu: in the options' order", i);
|
||||
CHECK(kEntries[i].id2 == settings::Id(size_t(kEntries[i].id) + 1), "entry %zu: its second key is the next option", i);
|
||||
CHECK(kEntries[i].id2 <= settings::Id::KeyRightStickClick2, "entry %zu: a Key option", i);
|
||||
if (kEntries[i].role == Role::Button)
|
||||
CHECK(kEntries[i].button != 0 && (kEntries[i].button & (kEntries[i].button - 1)) == 0, "entry %zu: one bit", i);
|
||||
else
|
||||
CHECK(kEntries[i].button == 0, "entry %zu: no bit", i);
|
||||
}
|
||||
CHECK(kEntries[size_t(Control::RightTrigger)].id == settings::Id::KeyRightTrigger &&
|
||||
kEntries[size_t(Control::RightStickClick)].id2 == settings::Id::KeyRightStickClick2, "first and last");
|
||||
// Either key's option finds its control.
|
||||
CHECK(&EntryOf(settings::Id::KeyRightTrigger) == &kEntries[0] && &EntryOf(settings::Id::KeyRightTrigger2) == &kEntries[0] &&
|
||||
&EntryOf(settings::Id::KeyLeftStickLeft2) == &kEntries[size_t(Control::LeftStickLeft)] &&
|
||||
&EntryOf(settings::Id::KeyRightStickClick2) == &kEntries[kControls - 1], "EntryOf");
|
||||
// The bits are XInput's (xbox.h's XAMINPUT_GAMEPAD_*; input.cpp asserts the same).
|
||||
CHECK(button::DpadUp == 0x0001 && button::DpadDown == 0x0002 && button::DpadLeft == 0x0004 && button::DpadRight == 0x0008 &&
|
||||
button::Start == 0x0010 && button::Back == 0x0020 && button::LeftThumb == 0x0040 && button::RightThumb == 0x0080 &&
|
||||
button::LeftShoulder == 0x0100 && button::RightShoulder == 0x0200 &&
|
||||
button::A == 0x1000 && button::B == 0x2000 && button::X == 0x4000 && button::Y == 0x8000, "XInput's bits");
|
||||
|
||||
// Nothing held: nothing.
|
||||
Resolved r = Press({});
|
||||
CHECK(r.buttons == 0 && !r.leftTrigger && !r.rightTrigger && NoAxis(r), "idle");
|
||||
|
||||
// A button key sets its bit and nothing else.
|
||||
r = Press({ Control::A });
|
||||
CHECK(r.buttons == button::A && !r.leftTrigger && !r.rightTrigger && NoAxis(r), "A: 0x%04x", r.buttons);
|
||||
CHECK(Press({ Control::B }).buttons == button::B && Press({ Control::X }).buttons == button::X && Press({ Control::Y }).buttons == button::Y, "B X Y");
|
||||
CHECK(Press({ Control::LB }).buttons == button::LeftShoulder && Press({ Control::RB }).buttons == button::RightShoulder, "LB RB");
|
||||
CHECK(Press({ Control::DpadUp }).buttons == button::DpadUp && Press({ Control::DpadDown }).buttons == button::DpadDown &&
|
||||
Press({ Control::DpadLeft }).buttons == button::DpadLeft && Press({ Control::DpadRight }).buttons == button::DpadRight, "D-pad");
|
||||
CHECK(Press({ Control::Start }).buttons == button::Start && Press({ Control::Back }).buttons == button::Back, "Start Back");
|
||||
CHECK(Press({ Control::LeftStickClick }).buttons == button::LeftThumb && Press({ Control::RightStickClick }).buttons == button::RightThumb, "clicks");
|
||||
r = Press({ Control::DpadLeft, Control::Back, Control::X });
|
||||
CHECK(r.buttons == (button::DpadLeft | button::Back | button::X), "several: 0x%04x", r.buttons);
|
||||
|
||||
// A trigger key is that trigger at full.
|
||||
r = Press({ Control::RightTrigger });
|
||||
CHECK(r.rightTrigger && !r.leftTrigger && r.buttons == 0 && NoAxis(r), "right trigger");
|
||||
r = Press({ Control::LeftTrigger });
|
||||
CHECK(r.leftTrigger && !r.rightTrigger, "left trigger");
|
||||
|
||||
// An axis is the difference of its keys, set while either is down, so
|
||||
// both keys centre the stick and no key leaves the controller's value.
|
||||
const size_t LX = size_t(Axis::LX), LY = size_t(Axis::LY), RX = size_t(Axis::RX), RY = size_t(Axis::RY);
|
||||
r = Press({ Control::LeftStickRight });
|
||||
CHECK(r.axisSet[LX] && r.axis[LX] == 1.0f && !r.axisSet[LY] && !r.axisSet[RX] && !r.axisSet[RY] && r.buttons == 0, "right: +1");
|
||||
r = Press({ Control::LeftStickLeft });
|
||||
CHECK(r.axisSet[LX] && r.axis[LX] == -1.0f, "left: -1");
|
||||
r = Press({ Control::LeftStickLeft, Control::LeftStickRight });
|
||||
CHECK(r.axisSet[LX] && r.axis[LX] == 0.0f, "both: set, centred");
|
||||
r = Press({ Control::LeftStickUp });
|
||||
CHECK(r.axisSet[LY] && r.axis[LY] == 1.0f && !r.axisSet[LX], "up is positive");
|
||||
r = Press({ Control::LeftStickDown });
|
||||
CHECK(r.axisSet[LY] && r.axis[LY] == -1.0f, "down is negative");
|
||||
r = Press({ Control::RightStickUp });
|
||||
CHECK(r.axisSet[RY] && r.axis[RY] == 1.0f && !r.axisSet[RX] && !r.axisSet[LY], "right stick up");
|
||||
r = Press({ Control::RightStickDown });
|
||||
CHECK(r.axisSet[RY] && r.axis[RY] == -1.0f, "right stick down");
|
||||
r = Press({ Control::RightStickLeft });
|
||||
CHECK(r.axisSet[RX] && r.axis[RX] == -1.0f && !r.axisSet[RY], "right stick left");
|
||||
r = Press({ Control::RightStickRight });
|
||||
CHECK(r.axisSet[RX] && r.axis[RX] == 1.0f, "right stick right");
|
||||
|
||||
// One key on a button and an axis (the default Up): both fire.
|
||||
r = Press({ Control::DpadUp, Control::LeftStickUp });
|
||||
CHECK(r.buttons == button::DpadUp && r.axisSet[LY] && r.axis[LY] == 1.0f, "Up: D-pad and stick");
|
||||
|
||||
// Everything at once is still each thing.
|
||||
bool all[kControls];
|
||||
for (bool& b : all)
|
||||
b = true;
|
||||
r = Resolve(all);
|
||||
CHECK(r.buttons == 0xF3FF && r.leftTrigger && r.rightTrigger, "all buttons: 0x%04x", r.buttons);
|
||||
for (size_t a = 0; a < size_t(Axis::Count); a++)
|
||||
CHECK(r.axisSet[a] && r.axis[a] == 0.0f, "axis %zu: both keys, centred", a);
|
||||
|
||||
// Usable at compile time (constexpr), so the table is checked when it's built.
|
||||
constexpr bool kNone[kControls] = {};
|
||||
static_assert(Resolve(kNone).buttons == 0, "constexpr");
|
||||
|
||||
printf("%s (%d failures)\n", g_failures ? "FAILED" : "passed", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Tests for user/key_names.h: the keys a Controls row can hold and their
|
||||
// names in settings.toml. Links SDL3 to check every code against SDL's name
|
||||
// for that scancode (no SDL_Init: the names are SDL's built-in ones, before
|
||||
// a platform renames any). From the repo root, with the deps in build/deps:
|
||||
// clang++ -std=c++20 -O2 -Iruntime -Ibuild/deps/include tests/key_names_test.cpp -Lbuild/deps/lib -lSDL3 \
|
||||
// -Wl,-rpath,build/deps/lib -o build/key_names_test && build/key_names_test
|
||||
#include <user/key_names.h>
|
||||
|
||||
#include <SDL3/SDL_keyboard.h>
|
||||
#include <SDL3/SDL_scancode.h>
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
using namespace settings::keynames;
|
||||
|
||||
int main()
|
||||
{
|
||||
CHECK(kKeyCount == 123, "%zu keys", kKeyCount);
|
||||
|
||||
// Tokens: lower-case letters, digits and underscores, each once. Codes:
|
||||
// scancodes, ascending, each once. And both ways round.
|
||||
std::set<std::string_view> tokens;
|
||||
uint16_t last = 0;
|
||||
for (const Key& k : kKeys)
|
||||
{
|
||||
bool wellFormed = k.token && k.token[0];
|
||||
for (const char* c = k.token; wellFormed && *c; c++)
|
||||
wellFormed = (*c >= 'a' && *c <= 'z') || (*c >= '0' && *c <= '9') || *c == '_';
|
||||
CHECK(wellFormed, "%s: a token is [a-z0-9_]+", k.token ? k.token : "(null)");
|
||||
CHECK(tokens.insert(k.token).second, "%s: once", k.token);
|
||||
CHECK(k.code > last, "%s (%d): codes ascend", k.token, k.code);
|
||||
last = k.code;
|
||||
CHECK(k.code != kNone && k.code != kUnknown && k.code < SDL_SCANCODE_COUNT, "%s: a scancode", k.token);
|
||||
CHECK(Token(k.code) == k.token, "%s: Token(code)", k.token);
|
||||
CHECK(Code(k.token) == k.code, "%s: Code(token)", k.token);
|
||||
CHECK(Known(k.code), "%s: Known", k.token);
|
||||
}
|
||||
CHECK(!tokens.count("none"), "\"none\" is not a row of the table");
|
||||
|
||||
// none, any case, and what isn't a name.
|
||||
CHECK(std::string_view(Token(kNone)) == "none" && Code("none") == kNone && Code("NONE") == kNone && Known(kNone), "none is 0");
|
||||
CHECK(Code("Left_Shift") == SDL_SCANCODE_LSHIFT && Code("W") == SDL_SCANCODE_W && Code("w") == SDL_SCANCODE_W, "any case");
|
||||
CHECK(Code("") == kUnknown && Code("Left Shift") == kUnknown && Code("left_shif") == kUnknown &&
|
||||
Code("left_shiftx") == kUnknown && Code("none ") == kUnknown, "not a token");
|
||||
CHECK(Token(1) == nullptr && Token(SDL_SCANCODE_LGUI) == nullptr && Token(kUnknown) == nullptr && Token(512) == nullptr, "not a key");
|
||||
CHECK(!Known(1) && !Known(SDL_SCANCODE_F1) && !Known(SDL_SCANCODE_LGUI) && !Known(SDL_SCANCODE_RGUI) && !Known(kUnknown),
|
||||
"F1, Cmd / Windows and nameless codes are out");
|
||||
CHECK(Code("f1") == kUnknown && Code("left_gui") == kUnknown && Code("right_gui") == kUnknown, "...by name too");
|
||||
CHECK(Code("escape") == SDL_SCANCODE_ESCAPE && Known(SDL_SCANCODE_ESCAPE), "Esc is a key (Back's default)");
|
||||
|
||||
// SDL agrees with every row: its name for the scancode, lowered with
|
||||
// spaces as underscores, is the token, bar the keys SDL names by their
|
||||
// symbol or runs together.
|
||||
const std::map<uint16_t, std::string_view> exceptions = {
|
||||
{ SDL_SCANCODE_MINUS, "minus" }, { SDL_SCANCODE_EQUALS, "equals" }, { SDL_SCANCODE_LEFTBRACKET, "left_bracket" },
|
||||
{ SDL_SCANCODE_RIGHTBRACKET, "right_bracket" }, { SDL_SCANCODE_BACKSLASH, "backslash" }, { SDL_SCANCODE_NONUSHASH, "non_us_hash" },
|
||||
{ SDL_SCANCODE_SEMICOLON, "semicolon" }, { SDL_SCANCODE_APOSTROPHE, "apostrophe" }, { SDL_SCANCODE_GRAVE, "grave" },
|
||||
{ SDL_SCANCODE_COMMA, "comma" }, { SDL_SCANCODE_PERIOD, "period" }, { SDL_SCANCODE_SLASH, "slash" },
|
||||
{ SDL_SCANCODE_CAPSLOCK, "caps_lock" }, { SDL_SCANCODE_PRINTSCREEN, "print_screen" }, { SDL_SCANCODE_SCROLLLOCK, "scroll_lock" },
|
||||
{ SDL_SCANCODE_PAGEUP, "page_up" }, { SDL_SCANCODE_PAGEDOWN, "page_down" }, { SDL_SCANCODE_NUMLOCKCLEAR, "num_lock" },
|
||||
{ SDL_SCANCODE_KP_DIVIDE, "keypad_divide" }, { SDL_SCANCODE_KP_MULTIPLY, "keypad_multiply" }, { SDL_SCANCODE_KP_MINUS, "keypad_minus" },
|
||||
{ SDL_SCANCODE_KP_PLUS, "keypad_plus" }, { SDL_SCANCODE_KP_PERIOD, "keypad_period" }, { SDL_SCANCODE_KP_EQUALS, "keypad_equals" },
|
||||
{ SDL_SCANCODE_KP_COMMA, "keypad_comma" }, { SDL_SCANCODE_NONUSBACKSLASH, "non_us_backslash" },
|
||||
};
|
||||
for (const Key& k : kKeys)
|
||||
{
|
||||
std::string expected;
|
||||
if (auto e = exceptions.find(k.code); e != exceptions.end())
|
||||
expected = e->second;
|
||||
else
|
||||
for (const char* c = SDL_GetScancodeName(SDL_Scancode(k.code)); *c; c++)
|
||||
expected += *c == ' ' ? '_' : char(std::tolower(uint8_t(*c)));
|
||||
CHECK(expected == k.token, "%d: SDL calls it \"%s\", the table \"%s\"", k.code, SDL_GetScancodeName(SDL_Scancode(k.code)), k.token);
|
||||
}
|
||||
|
||||
// Usable at compile time (the option table's defaults are checked when it's built).
|
||||
static_assert(Code("w") == 26 && Token(26)[0] == 'w' && Known(0) && !Known(227) && Code("None") == kNone, "constexpr");
|
||||
|
||||
printf("%s (%d failures)\n", g_failures ? "FAILED" : "passed", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// Tests for the Key options (Settings > Controls, user/settings.h): the
|
||||
// table, the [keys] names in settings.toml, and what the menu's Set accepts.
|
||||
// Links user/settings.cpp (and toml++ from XenonRecomp's vendored copy); no
|
||||
// SDL. From the repo root:
|
||||
// clang++ -std=c++20 -O2 -Iruntime -Itools/XenonRecomp/thirdparty/tomlplusplus/include \
|
||||
// tests/settings_keys_test.cpp runtime/user/settings.cpp -o build/settings_keys_test && build/settings_keys_test
|
||||
#include <user/key_names.h>
|
||||
#include <user/settings.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <unistd.h> // mkdtemp
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) do { if (!(cond)) { g_failures++; printf(" FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
static std::string ReadAll(const std::filesystem::path& file)
|
||||
{
|
||||
std::ifstream f(file);
|
||||
std::stringstream s;
|
||||
s << f.rdbuf();
|
||||
return s.str();
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
using namespace settings;
|
||||
using keynames::Code;
|
||||
|
||||
// The table: 48 Key options, every one in [keys], on the Controls page,
|
||||
// live, shown, with no environment variable and a default the key table
|
||||
// knows (none included).
|
||||
int keys = 0;
|
||||
std::set<std::string> names;
|
||||
for (const Option& o : Options())
|
||||
{
|
||||
if (o.type != Type::Key)
|
||||
{
|
||||
CHECK(std::string_view(o.table) != "keys", "%s.%s: only Key options live in [keys]", o.table, o.key);
|
||||
CHECK(o.env != nullptr, "%s.%s: every other option has an environment variable", o.table, o.key);
|
||||
continue;
|
||||
}
|
||||
keys++;
|
||||
CHECK(std::string_view(o.table) == "keys", "%s.%s: a Key option lives in [keys]", o.table, o.key);
|
||||
CHECK(o.category == Category::Controls, "%s: on the Controls page", o.key);
|
||||
CHECK(o.apply == Apply::Live && !o.hidden, "%s: live and shown", o.key);
|
||||
CHECK(o.env == nullptr, "%s: no environment variable", o.key);
|
||||
CHECK(names.insert(o.key).second, "%s: one row per key", o.key);
|
||||
CHECK(o.defaultValue >= 0 && o.defaultValue == std::floor(o.defaultValue) && keynames::Known(uint16_t(o.defaultValue)),
|
||||
"%s: the default is a key, or none", o.key);
|
||||
CHECK(o.min == 0 && o.max == 0 && o.step == 0 && o.entries.empty(), "%s: no range", o.key);
|
||||
}
|
||||
CHECK(keys == 48, "%d Key options", keys);
|
||||
CHECK(std::string_view(CategoryName(Category::Controls)) == "Controls", "the category's name");
|
||||
|
||||
// The defaults: v0.1.4's keys (the arrows steer as well as A and D, and
|
||||
// are the D-pad; O/L/K/; are the right stick), plus the left stick
|
||||
// click, which had none. In page order, in pairs: the second key is the first's id +
|
||||
// 1, named <key>_2, with the row's label.
|
||||
struct Default { Id id; const char* key; const char* label; const char* first; const char* second; };
|
||||
const Default defaults[] = {
|
||||
{ Id::KeyRightTrigger, "right_trigger", "Right Trigger", "w", "none" },
|
||||
{ Id::KeyLeftTrigger, "left_trigger", "Left Trigger", "s", "none" },
|
||||
{ Id::KeyLeftStickLeft, "left_stick_left", "Left Stick Left", "a", "left" },
|
||||
{ Id::KeyLeftStickRight, "left_stick_right", "Left Stick Right", "d", "right" },
|
||||
{ Id::KeyLeftStickUp, "left_stick_up", "Left Stick Up", "up", "none" },
|
||||
{ Id::KeyLeftStickDown, "left_stick_down", "Left Stick Down", "down", "none" },
|
||||
{ Id::KeyA, "a", "A", "space", "none" },
|
||||
{ Id::KeyB, "b", "B", "backspace", "none" },
|
||||
{ Id::KeyX, "x", "X", "left_shift", "none" },
|
||||
{ Id::KeyY, "y", "Y", "left_ctrl", "none" },
|
||||
{ Id::KeyLB, "lb", "LB", "q", "none" },
|
||||
{ Id::KeyRB, "rb", "RB", "e", "none" },
|
||||
{ Id::KeyDpadUp, "dpad_up", "D-pad Up", "up", "none" },
|
||||
{ Id::KeyDpadDown, "dpad_down", "D-pad Down", "down", "none" },
|
||||
{ Id::KeyDpadLeft, "dpad_left", "D-pad Left", "left", "none" },
|
||||
{ Id::KeyDpadRight, "dpad_right", "D-pad Right", "right", "none" },
|
||||
{ Id::KeyStart, "start", "Start", "return", "none" },
|
||||
{ Id::KeyBack, "back", "Back", "escape", "none" },
|
||||
{ Id::KeyRightStickUp, "right_stick_up", "Right Stick Up", "o", "none" },
|
||||
{ Id::KeyRightStickDown, "right_stick_down", "Right Stick Down", "l", "none" },
|
||||
{ Id::KeyRightStickLeft, "right_stick_left", "Right Stick Left", "k", "none" },
|
||||
{ Id::KeyRightStickRight, "right_stick_right", "Right Stick Right", "semicolon", "none" },
|
||||
{ Id::KeyLeftStickClick, "left_stick_click", "Left Stick Click", "z", "none" },
|
||||
{ Id::KeyRightStickClick, "right_stick_click", "Right Stick Click", "c", "none" },
|
||||
};
|
||||
size_t previous = size_t(Id::Vibration);
|
||||
for (const Default& d : defaults)
|
||||
{
|
||||
const Option& o = Info(d.id);
|
||||
const Option& o2 = Info(Id(size_t(d.id) + 1));
|
||||
CHECK(std::string_view(o.key) == d.key, "%s is %s", o.key, d.key);
|
||||
CHECK(std::string_view(o2.key) == std::string(d.key) + "_2", "%s: the second key is %s_2", o2.key, d.key);
|
||||
CHECK(o2.type == Type::Key, "%s: the second key is a Key option", o2.key);
|
||||
CHECK(std::string_view(o.label) == d.label && std::string_view(o2.label) == d.label, "%s: both keys carry the row's label", d.key);
|
||||
CHECK(o.defaultValue == double(Code(d.first)), "%s defaults to %s, not %s", o.key, d.first, FormatValue(o.id, o.defaultValue).c_str());
|
||||
CHECK(o2.defaultValue == double(Code(d.second)), "%s defaults to %s, not %s", o2.key, d.second, FormatValue(o2.id, o2.defaultValue).c_str());
|
||||
CHECK(size_t(d.id) == previous + 1, "%s follows the row before it", o.key);
|
||||
previous = size_t(d.id) + 1;
|
||||
}
|
||||
CHECK(size_t(Id::AsyncShaders) == previous + 1, "Advanced follows the keys");
|
||||
CHECK(EnvOverride(Id::KeyA) == nullptr, "nothing overrides a key");
|
||||
|
||||
// A scratch folder of our own.
|
||||
std::string pattern = (std::filesystem::temp_directory_path() / "speedbreaker-keys-XXXXXX").string();
|
||||
if (!mkdtemp(pattern.data()))
|
||||
{
|
||||
printf("can't make a scratch folder: %s\n", strerror(errno));
|
||||
return 2;
|
||||
}
|
||||
const std::filesystem::path dir = pattern;
|
||||
const std::filesystem::path file = dir / "settings.toml";
|
||||
|
||||
// The file: names in, in any case; "none" empties a key; anything else
|
||||
// keeps the default, SDL's spelling from the first build included.
|
||||
{
|
||||
std::ofstream f(file);
|
||||
f << "[keys]\n"
|
||||
"right_trigger = \"up\"\n"
|
||||
"left_trigger = \"Keypad_1\"\n"
|
||||
"left_stick_left_2 = \"none\"\n"
|
||||
"right_stick_left = \"K\"\n"
|
||||
"right_stick_up = \"LEFT_SHIFT\"\n"
|
||||
"a = \"Left Shift\"\n"
|
||||
"b = 42\n"
|
||||
"x = true\n"
|
||||
"y = \"\"\n"
|
||||
"lb = \"f1\"\n"
|
||||
"rb = \"left_gui\"\n";
|
||||
}
|
||||
CHECK(Load(file), "loads: %s", LastError().c_str());
|
||||
CHECK(GetInt(Id::KeyRightTrigger) == Code("up"), "\"up\": %d", GetInt(Id::KeyRightTrigger));
|
||||
CHECK(GetInt(Id::KeyLeftTrigger) == Code("keypad_1"), "a token in another case: %d", GetInt(Id::KeyLeftTrigger));
|
||||
CHECK(GetInt(Id::KeyLeftStickLeft2) == keynames::kNone, "\"none\" is no key: %d", GetInt(Id::KeyLeftStickLeft2));
|
||||
CHECK(GetInt(Id::KeyRightStickLeft) == Code("k"), "\"K\" is k: %d", GetInt(Id::KeyRightStickLeft));
|
||||
CHECK(GetInt(Id::KeyRightStickUp) == Code("left_shift"), "\"LEFT_SHIFT\" is left_shift: %d", GetInt(Id::KeyRightStickUp));
|
||||
CHECK(GetInt(Id::KeyA) == Code("space"), "SDL's spelling keeps the default");
|
||||
CHECK(GetInt(Id::KeyB) == Code("backspace"), "a number keeps the default");
|
||||
CHECK(GetInt(Id::KeyX) == Code("left_shift"), "a boolean keeps the default");
|
||||
CHECK(GetInt(Id::KeyY) == Code("left_ctrl"), "an empty name keeps the default");
|
||||
CHECK(GetInt(Id::KeyLB) == Code("q") && GetInt(Id::KeyRB) == Code("e"), "F1 and Cmd / Windows can't be bound from the file");
|
||||
CHECK(GetInt(Id::KeyStart) == Code("return") && GetInt(Id::KeyLeftStickRight2) == Code("right"), "a missing key keeps the default");
|
||||
CHECK(IsDefault(Id::KeyLB) && !IsDefault(Id::KeyRightTrigger) && !IsDefault(Id::KeyLeftStickLeft2), "IsDefault follows the file");
|
||||
|
||||
// Shown in the log and the bug report by name.
|
||||
CHECK(FormatValue(Id::KeyRightTrigger, GetValue(Id::KeyRightTrigger)) == "up", "FormatValue: %s",
|
||||
FormatValue(Id::KeyRightTrigger, GetValue(Id::KeyRightTrigger)).c_str());
|
||||
CHECK(FormatValue(Id::KeyX, Code("left_shift")) == "left_shift" && FormatValue(Id::KeyX, 0) == "none", "a token, or none");
|
||||
CHECK(FormatValue(Id::KeyA, 1) == "?" && FormatValue(Id::KeyA, 227) == "?" && FormatValue(Id::KeyA, 1e9) == "?" &&
|
||||
FormatValue(Id::KeyA, NAN) == "?" && FormatValue(Id::KeyA, -1) == "?", "not a key: ?");
|
||||
|
||||
// Set: a key of the table, or none; anything else is Invalid.
|
||||
CHECK(Set(Id::KeyA, Code("return")) == SetResult::Applied, "a key");
|
||||
CHECK(Set(Id::KeyA, Code("return")) == SetResult::Unchanged, "the same key again");
|
||||
CHECK(Set(Id::KeyA, keynames::kNone) == SetResult::Applied && GetInt(Id::KeyA) == 0, "none");
|
||||
CHECK(Set(Id::KeyA2, Code("return")) == SetResult::Applied && Set(Id::KeyA, Code("return")) == SetResult::Applied,
|
||||
"the same key in both cells of a row is allowed");
|
||||
CHECK(Set(Id::KeyA2, keynames::kNone) == SetResult::Applied, "and so is none in both");
|
||||
CHECK(Set(Id::KeyA, 1) == SetResult::Invalid, "1 is no key");
|
||||
CHECK(Set(Id::KeyA, 58) == SetResult::Invalid, "F1 is no key");
|
||||
CHECK(Set(Id::KeyA, 227) == SetResult::Invalid, "Left GUI is no key");
|
||||
CHECK(Set(Id::KeyA, keynames::kUnknown) == SetResult::Invalid && Set(Id::KeyA, 1e9) == SetResult::Invalid &&
|
||||
Set(Id::KeyA, -1) == SetResult::Invalid, "out of the table");
|
||||
CHECK(Set(Id::KeyA, double(Code("return")) + 0.5) == SetResult::Invalid, "not a whole number");
|
||||
CHECK(Set(Id::KeyA, NAN) == SetResult::Invalid, "NaN");
|
||||
CHECK(GetInt(Id::KeyA) == Code("return"), "invalid sets change nothing: %d", GetInt(Id::KeyA));
|
||||
CHECK(StepValue(Id::KeyA, Code("return"), 1) == double(Code("return")) && StepValue(Id::KeyA, Code("return"), -1) == double(Code("return")),
|
||||
"Left and Right do nothing to a key");
|
||||
CHECK(!IsDefault(Id::KeyA) && Reset(Id::KeyA) == SetResult::Applied && IsDefault(Id::KeyA) && GetInt(Id::KeyA) == Code("space"), "Reset");
|
||||
|
||||
// Save: every key by name, the comment, no environment variable; and
|
||||
// what it wrote loads back.
|
||||
CHECK(Save(file), "saves: %s", LastError().c_str());
|
||||
std::string text = ReadAll(file);
|
||||
size_t at = text.find("\n[keys]\n"), advanced = text.find("\n[advanced]\n");
|
||||
CHECK(at != std::string::npos && advanced != std::string::npos && at < advanced, "[keys] before [advanced]");
|
||||
std::string section = at == std::string::npos ? "" : text.substr(at, advanced == std::string::npos ? std::string::npos : advanced - at);
|
||||
CHECK(section.find("right_trigger = \"up\"\n") != std::string::npos, "saved by name");
|
||||
CHECK(section.find("left_trigger = \"keypad_1\"\n") != std::string::npos, "the table's case, not the file's");
|
||||
CHECK(section.find("left_stick_left_2 = \"none\"\n") != std::string::npos, "an emptied key");
|
||||
CHECK(section.find("right_stick_left = \"k\"\n") != std::string::npos, "lower case");
|
||||
CHECK(section.find("right_stick_right = \"semicolon\"\n") != std::string::npos, "a symbol key by its name");
|
||||
CHECK(section.find("right_stick_up = \"left_shift\"\n") != std::string::npos, "a modifier");
|
||||
CHECK(section.find("left_stick_left = \"a\"\n") != std::string::npos && section.find("dpad_left_2 = \"none\"\n") != std::string::npos,
|
||||
"defaults are written too, both keys");
|
||||
CHECK(section.find("# A key name such as \"w\"") != std::string::npos, "the comment says what goes here");
|
||||
CHECK(section.find("NFSMW_") == std::string::npos, "no environment variable on a key");
|
||||
CHECK(text.find("(NFSMW_VIBRATION)") != std::string::npos, "the other options keep theirs");
|
||||
CHECK(Load(file), "loads what it saved: %s", LastError().c_str());
|
||||
CHECK(GetInt(Id::KeyRightTrigger) == Code("up") && GetInt(Id::KeyLeftTrigger) == Code("keypad_1") &&
|
||||
GetInt(Id::KeyRightStickUp) == Code("left_shift") && GetInt(Id::KeyLeftStickLeft2) == 0, "round trip");
|
||||
CHECK(!Dirty(), "nothing changed since the save");
|
||||
|
||||
// The keys SDL names by a symbol are words here: nothing to escape.
|
||||
CHECK(Set(Id::KeyA, Code("backslash")) == SetResult::Applied && Set(Id::KeyB, Code("apostrophe")) == SetResult::Applied,
|
||||
"the backslash and apostrophe keys are keys");
|
||||
CHECK(Save(file), "saves them: %s", LastError().c_str());
|
||||
text = ReadAll(file);
|
||||
CHECK(text.find("\na = \"backslash\"\n") != std::string::npos && text.find("\nb = \"apostrophe\"\n") != std::string::npos, "by name");
|
||||
CHECK(Load(file), "and the file still parses: %s", LastError().c_str());
|
||||
CHECK(GetInt(Id::KeyA) == Code("backslash") && GetInt(Id::KeyB) == Code("apostrophe"), "round trip: %d %d", GetInt(Id::KeyA), GetInt(Id::KeyB));
|
||||
CHECK(GetInt(Id::KeyRightTrigger) == Code("up") && GetInt(Id::MasterVolume) == 100, "and the rest of the file survived");
|
||||
std::filesystem::remove_all(dir);
|
||||
|
||||
printf("%s (%d failures)\n", g_failures ? "FAILED" : "passed", g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// SpeedBreaker tests. GPL-3.0-or-later (see COPYING).
|
||||
//
|
||||
// Settings > Graphics > Upscaling's values in settings.toml (user/settings.cpp,
|
||||
// linked as the game links it): "fsr1" (FSR 1, v0.1.5) read, saved and read
|
||||
// back, next to "bicubic" and "linear"; the menu's order (Bicubic, Bilinear,
|
||||
// FSR 1); NFSMW_SCALING=fsr1 for one run, never saved; a token this version
|
||||
// doesn't know (a later version's) falls back to the default, Bicubic; and
|
||||
// Sharpening's default, 0.3, unchanged.
|
||||
// The argument is a scratch folder (settings.toml is written there).
|
||||
//
|
||||
// Build by hand (from the repo root):
|
||||
// c++ -std=c++20 -O2 -UNDEBUG -I runtime tests/settings_scaling_test.cpp runtime/user/settings.cpp -o build/settings_scaling_test
|
||||
// build/settings_scaling_test build/settings_scaling_scratch
|
||||
#include <user/settings.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
int s_failures = 0;
|
||||
|
||||
#define CHECK(cond, ...) \
|
||||
do \
|
||||
{ \
|
||||
if (!(cond)) \
|
||||
{ \
|
||||
s_failures++; \
|
||||
fprintf(stderr, "FAIL %s:%d: %s: ", __FILE__, __LINE__, #cond); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fprintf(stderr, "\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
using settings::Id;
|
||||
using settings::ScalingMode;
|
||||
|
||||
std::string Read(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
std::stringstream text;
|
||||
text << in.rdbuf();
|
||||
return text.str();
|
||||
}
|
||||
|
||||
void Write(const std::filesystem::path& path, const std::string& text)
|
||||
{
|
||||
std::ofstream(path, std::ios::binary) << text;
|
||||
}
|
||||
|
||||
int32_t Scaling() { return settings::GetInt(Id::Scaling); }
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
unsetenv("NFSMW_SCALING");
|
||||
unsetenv("NFSMW_SHARPEN");
|
||||
const std::filesystem::path dir = argc > 1 ? argv[1] : "settings_scaling_scratch";
|
||||
std::filesystem::remove_all(dir);
|
||||
std::filesystem::create_directories(dir);
|
||||
const std::filesystem::path file = dir / "settings.toml";
|
||||
|
||||
// The option: three entries in menu order, Bicubic the default.
|
||||
const settings::Option& o = settings::Info(Id::Scaling);
|
||||
CHECK(o.entries.size() == 3, "%zu entries", o.entries.size());
|
||||
if (o.entries.size() == 3)
|
||||
{
|
||||
CHECK(o.entries[0].value == int32_t(ScalingMode::Bicubic) && std::string(o.entries[0].token) == "bicubic", "first: bicubic");
|
||||
CHECK(o.entries[1].value == int32_t(ScalingMode::Linear) && std::string(o.entries[1].token) == "linear", "second: linear");
|
||||
CHECK(o.entries[2].value == int32_t(ScalingMode::Fsr1) && std::string(o.entries[2].token) == "fsr1", "third: fsr1");
|
||||
}
|
||||
CHECK(settings::DefaultValue(Id::Scaling) == double(ScalingMode::Bicubic), "default bicubic");
|
||||
CHECK(settings::FormatValue(Id::Scaling, double(ScalingMode::Fsr1)) == "FSR 1", "label %s", settings::FormatValue(Id::Scaling, 2).c_str());
|
||||
CHECK(settings::StepValue(Id::Scaling, double(ScalingMode::Linear), 1) == double(ScalingMode::Fsr1), "Bilinear, right: FSR 1");
|
||||
CHECK(settings::StepValue(Id::Scaling, double(ScalingMode::Fsr1), -1) == double(ScalingMode::Linear), "FSR 1, left: Bilinear");
|
||||
|
||||
// No file: the defaults.
|
||||
CHECK(settings::Load(file), "load without a file: %s", settings::LastError().c_str());
|
||||
CHECK(Scaling() == int32_t(ScalingMode::Bicubic), "no file: %d", Scaling());
|
||||
CHECK(std::fabs(settings::GetFloat(Id::Sharpening) - 0.3f) < 1e-6f, "Sharpening's default 0.3: %.3f", settings::GetFloat(Id::Sharpening));
|
||||
|
||||
// Chosen in the menu, saved, read back.
|
||||
CHECK(settings::Set(Id::Scaling, double(ScalingMode::Fsr1)) == settings::SetResult::Applied, "set FSR 1");
|
||||
CHECK(Scaling() == int32_t(ScalingMode::Fsr1), "in effect at once: %d", Scaling());
|
||||
CHECK(settings::Save(file), "save: %s", settings::LastError().c_str());
|
||||
const std::string saved = Read(file);
|
||||
CHECK(saved.find("scaling = \"fsr1\"") != std::string::npos, "saved as a token:\n%s", saved.c_str());
|
||||
CHECK(settings::Set(Id::Scaling, double(ScalingMode::Linear)) == settings::SetResult::Applied, "set bilinear");
|
||||
CHECK(settings::Load(file) && Scaling() == int32_t(ScalingMode::Fsr1), "read back: %d", Scaling());
|
||||
|
||||
// Each token by hand.
|
||||
for (auto [token, value] : { std::pair{ "bicubic", ScalingMode::Bicubic }, std::pair{ "linear", ScalingMode::Linear }, std::pair{ "fsr1", ScalingMode::Fsr1 } })
|
||||
{
|
||||
Write(file, std::string("[video]\nscaling = \"") + token + "\"\n");
|
||||
CHECK(settings::Load(file) && Scaling() == int32_t(value), "%s: %d", token, Scaling());
|
||||
}
|
||||
// A later version's value: the default, and the file isn't rewritten by loading it.
|
||||
Write(file, "[video]\nscaling = \"fsr3\"\n");
|
||||
settings::Load(file);
|
||||
CHECK(Scaling() == int32_t(ScalingMode::Bicubic), "unknown token: %d", Scaling());
|
||||
CHECK(Read(file).find("fsr3") != std::string::npos, "the file is left as it was");
|
||||
|
||||
// NFSMW_SCALING=fsr1 for this run: in effect, never saved.
|
||||
Write(file, "[video]\nscaling = \"linear\"\n");
|
||||
setenv("NFSMW_SCALING", "fsr1", 1);
|
||||
CHECK(settings::Load(file), "load with the environment");
|
||||
CHECK(Scaling() == int32_t(ScalingMode::Fsr1), "environment: %d", Scaling());
|
||||
CHECK(settings::EnvOverride(Id::Scaling) != nullptr, "the menu shows the override");
|
||||
CHECK(settings::GetStoredValue(Id::Scaling) == double(ScalingMode::Linear), "stored: %g", settings::GetStoredValue(Id::Scaling));
|
||||
CHECK(settings::Save(file) && Read(file).find("scaling = \"linear\"") != std::string::npos, "saved: the file's value:\n%s", Read(file).c_str());
|
||||
unsetenv("NFSMW_SCALING");
|
||||
|
||||
std::filesystem::remove_all(dir);
|
||||
if (s_failures)
|
||||
{
|
||||
printf("%d check(s) FAILED\n", s_failures);
|
||||
return 1;
|
||||
}
|
||||
printf("all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in new issue
Block a user