48 Commits
Author SHA1 Message Date
Valiantsin DzerakhandClaude Opus 5.5 adf9c768c3 Raise the version to 0.1.9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:20:31 +03:00
Valiantsin Dzerakh 9044d4c02b Merge task/xboxprompts: Xbox button prompts layout, from pull request #126
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	installer/steps.py
#	runtime/romfs.c
#	tools/make_romfs_patches.py
2026-10-31 08:19:29 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f705ceff9f Merge task/ultrawide: game resolutions wider than 16:9 on 1.4.3, from pull request #137 (#6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 08:16:12 +03:00
Valiantsin DzerakhandClaude Opus 5.5 61dccff1a0 Say that the Xbox prompts do not follow remapped buttons
The swapped letters are baked into the game's textures, fonts and messages, so a button remapped with a bind_ line or on the Controls tab keeps the old letter on screen. The menu description (English and Russian) and docs/controls.md now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:52:59 +03:00
Valiantsin DzerakhandClaude Opus 5.5 301fc19aa3 Fall back to romfs-patches when romfs-patches-xbox fails its check at boot
The Xbox prompts folder is optional, so a manifest that no longer matches the RomFS logs the reason and drops that folder instead of stopping the game. The main romfs-patches and the mods folder keep their fatal checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:52:41 +03:00
mrtink125792andClaude Opus 5.5 7923e2da6e Let the setup go on without the Xbox prompts, and put the Xbox prompts docs below the controls table
romfs-patches-xbox is optional: when make_romfs_patches.py --xbox-prompts fails (a game update whose prompts it does
not recognise), the setup removes the partial folder, warns (warn.xbox_prompts_failed) and finishes, where it stopped
the whole install before. The game then keeps the Switch prompts and the menu says the files are missing. Its log is
prepare-xbox-make_romfs_patches.log, so it no longer replaces the 60 fps run's log, and Prepare no longer counts the
folder as part of a finished step.

The Xbox button prompts section of docs/controls.md sat between the layout list and the controls table it introduces;
it now follows the table. The building.md note follows the paragraph it interrupted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:51:03 +03:00
mrtink125792andClaude Opus 5.5 bb85bc8a80 Add an Xbox prompts button layout: Switch positions with the A/B and X/Y prompts swapped to the Xbox labels
With pad_layout = nintendo on an Xbox pad the right button does Switch A but is labelled B, the bottom one does B but is
labelled A, and the game's prompts still name the Switch buttons. A fourth Button layout choice, Xbox prompts (A right)
(pad_layout = xbox_prompts), maps the buttons as nintendo and swaps the prompts to the labels, so each prompt names the
button to press. Automatic, Nintendo and Xbox are unchanged and keep the game's own prompts. The buttons change at once;
the prompts at the next start, as the game reads its UI archive and fonts at boot (the menu says so).

At start, xbox_prompts loads romfs-patches-xbox in place of romfs-patches (runtime/romfs.c overlay_init).
tools/make_romfs_patches.py --xbox-prompts writes it from the player's own RomFS, with the 60 fps patches; nothing from
the game or a mod is added to the repository. It swaps A with B and X with Y in:
- the letter images Nt_KeyTexA/B/X/Y_00 of the action prompt (DoCommand_00), in the Common archive's __Combined.bntx
  (same format, size and tiling, so the image data trades places);
- the action prompt's diamond of the other face buttons: one-letter text panes the game fills from
  LayoutMsg/DoCommand_00.msbt in each language's Mals archive (both the messages and the layout's own text);
- the button glyphs U+E040 to U+E043 of scft/External.bfotf in each Font/Font*.bfarc, which every other prompt uses
  (CFF outlines and metrics in the deobfuscated OpenType font, then its checksums).
The diamond's shape shows the button's position and stays. The setup writes both folders; the menu notes when the Xbox
prompt files are missing and when the prompts in use differ from the choice until a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:51:03 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9745206f2a Document the ultrawide sizes, the fit default of aspect and the 1.4.3 limit
Wide sizes other than the four the installer prepares get the wide camera with a 16:9 HUD stretched over it. aspect = fit letterboxes a 16:9 game in a wider window where it used to stretch. Ultrawide is 1.4.3 only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:45:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 09da9f1687 Credit the TOTK-Ultrawide mod for the pane list and offsets in the third-party notices
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:45:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b63d4fbf55 Check the root pane scale in ultrawide_layout.py, report a missing member clearly and fix its docstring
The root pane's shipped scale_x must be 1.0 before the tool writes the narrowed scale. A layout or animation member missing from the archive stops with a SystemExit naming it instead of a bare KeyError. The docstring no longer describes nvn_mtl.m's UI squeeze or scripts/ paths from upstream, and says where the pane list and distances come from (the GPL TOTK-Ultrawide mod, values only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:45:21 +03:00
Valiantsin DzerakhandClaude Opus 5.5 583f4b45c2 Build the wide UI overlays for 1.4.3 only and share the 16:9 test between resolution.c and romfs.c
1.2.1 has no camera aspect patch, so the installer no longer builds romfs-patches-<w>x<h> for it (the layout tool is checked against 1.4.3 and could fail the whole install), and romfs.c overlay_init skips that overlay on 1.2.1 through res_wants_wide_overlay. overlay_init and res_init now both call res_is_16x9 (1% tolerance), so 1366x768 counts as 16:9 and logs no missing overlay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:44:30 +03:00
Seth 582f675e88 Render a game resolution wider than 16:9 at its own shape, with black bars as the fallback
An output_res such as 3440x1440 now widens the camera's view to that shape on 1.4.3: patches-v143.txt
sets the aspect ratio at the camera and viewport sites that hand it to the perspective projection.
The HUD and menus come from a romfs-patches-<w>x<h> overlay that make_romfs_patches.py --aspect
builds through tools/ultrawide_layout.py, and romfs.c overlay_init picks that overlay when the game
resolution is not 16:9. The installer prepares it for 2560x1080, 3440x1440, 3840x1600 and 5120x2160.

When the window's shape differs from the game resolution's, the new aspect setting keeps the
picture's shape with black bars (fit, the default) or stretches it over the window.
2026-10-31 07:43:44 +03:00
Valiantsin DzerakhandClaude Opus 5.5 87e0275991 Credit MacNativeGames for the 1.4.3 fade fix in the third-party notices
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:43:02 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4e1edd7584 Merge task/astcbc: ASTC textures encoded to BC3 on request, from pull request #87
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:38:24 +03:00
Valiantsin DzerakhandClaude Opus 5.5 fc133df1c0 Merge task/shaderpack: shader cache in one pack file from pull request #64
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:36:02 +03:00
Valiantsin DzerakhandClaude Opus 5.5 67b1c03b0d Skip BC3 texel writes that end inside a block, and check the BC3 encoder in --probe-astc
A BC3 image takes whole 4x4 blocks or a part ending on the mip's edge; a WriteTexels region of a 5x5 or 6x6 footprint may end inside one. --probe-astc encodes every footprint to BC3 and holds the blocks to alpha, palette-place, flat-colour and PSNR bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:35:00 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c67ef58763 Build the BC3 encoder header with tools/build_shaders.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:34:59 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b69f44be78 Merge task/paddles: bindable back paddles from pull request #156 (#140)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:29:30 +03:00
arthurhfcardosoandClaude Sonnet 5.5 dd496bfd5d Encode decoded ASTC textures to BC3 on GPUs without ASTC (astc_bc)
A GPU that cannot sample ASTC gets the textures decoded to RGBA8, four times the video memory of the ASTC blocks. With astc_bc = 1 (default 0, GPU decoder only) a second compute pass, bc3_encode.comp, encodes the decoded texels to BC3 and the image is BC3_UNORM or BC3_SRGB: 8 bits per texel instead of 32. The encoder is the usual bounding-box method with the box diagonal turned to the sign of the colour covariance, and the eight-value alpha mode.

A region of a WriteTexels that does not start on a 4x4 block corner is left as it was and logged. The setting is in totk.ini and the Advanced tab. The SPIR-V header was built with glslang 16.6 (build_shaders.py uses glslc) with the same source hash.

On a GTX 1650 (4 GiB) in the Fire Temple the GPU time per frame went from 10-23 ms to 3-12 ms and the frame rate held at 59-60 fps.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 07:27:46 +03:00
Valiantsin DzerakhandClaude Opus 5.5 202ace4e9e Name the back paddles in the pad text order comment
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:25:46 +03:00
Seth 70a83837b0 Let the back paddles be bound
SDL reports the back buttons of an Xbox Elite, DualSense Edge or Steam
Controller as four paddle buttons, but the pad table stopped at the
triggers, so they could not be mapped. They are now pad inputs lpaddle1,
rpaddle1, lpaddle2 and rpaddle2, with no default action, bindable from
totk.ini or the remapping rows.
2026-10-31 07:25:27 +03:00
Pasha Kagan 4d871c7920 Describe the shader cache pack in the installer documentation 2026-10-31 07:24:10 +03:00
Pasha Kagan 58b15ed069 Copy tools/pack_shader_cache.py into the release 2026-10-31 07:24:10 +03:00
Pasha Kagan 82af209b2b Add the Russian texts of the pack phase and its failure 2026-10-31 07:23:59 +03:00
Pasha Kagan 06ae18fe93 Add the English texts of the pack phase and its failure 2026-10-31 07:23:53 +03:00
Pasha Kagan dc5bd01a15 List tools/pack_shader_cache.py among the files a release needs 2026-10-31 07:23:40 +03:00
Pasha Kagan 00da267dcd Run tools/pack_shader_cache.py at the end of the prepare step 2026-10-31 07:23:40 +03:00
Pasha Kagan 3c749df3f4 Test tools/pack_shader_cache.py on a fake shader cache 2026-10-31 07:23:40 +03:00
Pasha Kagan bc84ac0b1f Add tools/pack_shader_cache.py, which folds the shader cache's loose files into shader-cache.pack 2026-10-31 07:23:40 +03:00
Pasha Kagan 2fcb09d609 Read shader translations, use masks and stored inputs from shader-cache.pack before the loose files 2026-10-31 07:23:40 +03:00
Valiantsin DzerakhandClaude Opus 5.5 ff25b04cdb Merge task/fatalmem: memory commit line on fatal errors (#94)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:23:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 31fff74776 Log the memory commit charge with every fatal error
#94 ended on a realloc of 1 MiB that failed with 64 GB of commit allowed, and the log could not tell whether the
process had grown towards the limit or the system ran short. rt_fatal now adds one line with the process's private
bytes and the system's commit in use and limit (plat_commit_info).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:23:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f958a5ef71 Merge task/romfscheck: fail the install when romfs/ is unreadable after cleanup (#107)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:12:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a0ba5dfb99 Merge task/deadzone: virtual test gamepad for stick tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:12:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 753b61ebe1 Merge task/audiolat: pace audio rendering by the output device (#98)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:12:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 ada341e2b1 Pace audio rendering by the output device
The render thread ran on its own 5 ms timer while the device consumed on its own clock, so the queue crept: on
DirectSound it grew 2.7 ms every 5 s, which fills the 340 ms ring in about ten minutes of play. The render thread now
renders the next frame when the device has taken the ring below max(prefill, two device periods), as the audio
hardware paces the Switch's DSP and as LASRecomp and SavageWindRecomp pace their renderers by the SDL stream. Without a
device, while the renderer is stopped and while the device takes nothing for 50 ms (at least four periods) it keeps the
timer, so a stalled device does not stall the game; the ring trim then drops what piled up, measured from the target.

60 s on the monsters save: WASAPI 15 ms queued, DirectSound 27-40 ms without growth, no underruns. With the SDL audio
thread suspended for 1.5 s the game held 60 fps and the queue was back at 15 ms after one trim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 07:12:48 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9016b1e210 Add a virtual test gamepad for stick tests without a controller
TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry> attaches an SDL virtual gamepad at start with its sticks at those raw values, so the
dead zone and the gamepad path can be tested in a run. On the field save: an 11% drift reached the game as 3.3% at the
default 8% zone (the old per-axis cutoff passed 9% on x and dropped y) and not at all at 15% (Link stood still); full
deflection reached it as full at 15%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:46:24 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a76224f8a6 Fail the install when romfs/ is gone or unreadable after cleanup
An install whose romfs/ went empty and unreadable after the extraction had verified it (a folder Windows was still
deleting) ended with result ok, and the game stopped at its first file read. Cleanup now checks that romfs.bin and
index.json are still there, match the verified size and hash record, and that romfs.bin opens; otherwise it fails with
cleanup.romfs_unreadable, which tells the player to restart Windows and run the installer on the same folder (resume
extracts the game data again). Reported in #107.

tests/installer_test.py CleanupTest: an emptied, removed or resized romfs passed cleanup before, fails now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:42:03 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8ba0d5ce15 Merge task/amdfix: 3D BC stand-ins and recorder-ended parts for AMD RDNA2 (#5)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:39:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 efbff61306 Merge task/lowres: 1280x720 and 1600x900 game resolutions in the menu (#36)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:26:39 +03:00
Valiantsin DzerakhandClaude Opus 5.5 be4d8f5245 Merge task/sixaxis: drop six-axis samples from before StartSixAxisSensor (#104)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:26:39 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a90b1f4ca9 Merge task/audiolat: skip audio piled up after a device stall (#98)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:26:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 58e52aa180 Merge task/deadzone: stick dead zone setting (#12 #99 #111 #115)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:26:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 605662ca87 Offer 1280x720 and 1600x900 as game resolutions in the menu
The game resolution row started at 1920x1080 although resolution.c accepts sizes from 1280x720, so weak GPUs and
integrated graphics had to edit totk.ini to draw the game smaller. A 1280x720 run on 1.4.3 renders the field with a
1067x600 scene and an intact interface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:25:05 +03:00
Valiantsin DzerakhandClaude Opus 5.5 7d89fde2f0 Drop the six-axis samples from before StartSixAxisSensor
motion_restart only flagged the next sample to start a fresh frame, so a read in the frame of the Start returned the
previous session's 16 states as live, and for the next 60 ms 15 of the 16 were from before it, with the angle jumping
back to zero between two reads. Motion aim was dead when the bow was first drawn and lagged after that. A restart now
hides the older samples and zeroes the angle; sampling numbers keep counting.

--probe-services checks a restart in the middle of a turn: 16 old states in the Start frame before, none after.
Reported with the cause and a harness in #104.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:22:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 42a1d32710 Skip audio that piles up in the output ring so the sound delay stays short
A stalled audio device (a wireless headset waking up, a change of the default device) filled the 340 ms ring, the
render thread then dropped new frames and the queue stayed full for the rest of the session: about a third of a second
of delay on top of the device's own. ring_pull now skips what is queued beyond the prefill, the device's request and
40 ms. The 5 s level line (trace = audio) reports the queue and the skipped frames.

With the SDL audio thread suspended for 1.5 s: before, 325-340 ms queued for the rest of the run; after, 10 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:20:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 ed1973a68b Add a stick dead zone setting for drifting gamepads
stick_deadzone (0 to 50 percent, default 8) replaces the fixed per-axis cutoff of 2500. The zone is round and the
travel past it is scaled to start at 0, so a drifting stick rests centred and full deflection still reaches full.
The Controls tab shows it as a slider; --probe-cfg checks the math (input_pad_stick).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:05:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 25b200f94c Draw 3D BC textures through stand-ins and let the recorder end its parts
AMD driver 26.10.1 on RDNA2 cards wrote past the end of a 3D BC4 volume made
BLOCK_TEXEL_VIEW_COMPATIBLE and 2D_ARRAY_COMPATIBLE, which lost the device at the
logo (issue #5). 3D BC textures no longer get block views: draws into a level go
to an uncompressed 2D array stand-in that is copied back at the pass end
(TOTK_BLOCK_VIEWS_3D=1 restores the old path).

Parts the recorder thread recorded and the submitting thread ended turned the
scene green, then white, on the same driver once a submission was split. The
recorder now writes the closing breadcrumb and ends the part itself
(TOTK_REC_END=0 restores the old split). The recorder stays off on AMD by default.

Based on the patch Pelipooper posted in issue #5; stand-ins extended to every
mip level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:50:02 +03:00
59 changed files with 2319 additions and 176 deletions

No files matched your search

+2 -1
View File
@@ -21,13 +21,14 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
- Custom resolutions (1080p, 2k...), supersampling and AMD FSR 1 upscaling
- In-game settings menu (F1) and performance overlay (F10)
- Keyboard, mouse and gamepad with remapping, rumble and gyro
- Xbox button prompts: a layout that keeps the Switch button positions and shows the Xbox labels
- Shaders prepared at install instead of during play
## Planned
- Linux and the Steam Deck
- Mods made for emulators: unpack a mod into a `mods` folder
- Custom aspect ratios, including ultrawide
- Ultrawide on 1.2.1 (1.4.3 has it for 2560x1080, 3440x1440, 3840x1600 and 5120x2160; other shapes get the wide camera with a 16:9 HUD)
- VRR
## Installing
+3
View File
@@ -9,6 +9,9 @@ Code copied or adapted from another project is listed here with its source, the
| LAS Recomp host runtime: NSO loader, host-function table, guest-space reservation and re-execution, guest thread start | https://github.com/peiikai/LASRecomp @ `da35b83a9611d046a9a681aae668d7085954d37f`, Copyright (c) 2026 Peiikai | MIT | `runtime/loader.c`, `runtime/hostfn.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/boot.c`; `runtime/vk_device.c`, `runtime/gfx_window.c`, `runtime/tex_astc.c` (`runtime/nvn/astc.c`; its decode ported to GLSL in `runtime/shaders/astc_decode.comp`, compiled into `runtime/astc_decode_spv.h`, and its tables copied into `runtime/vk_astc.c`), `runtime/vk_formats.c` (format table of `runtime/nvn/vulkan/textures.c`), `runtime/vk_memory.c` (page write generations of `runtime/nvn/vulkan/exec.c`, deferred destruction of `textures.c`), `runtime/vk_texture.c` (image creation, uploads and samplers of `runtime/nvn/vulkan/textures.c`), `runtime/vk_sync.c` (timeline submissions and waits of `runtime/nvn/vulkan/exec.c`), `runtime/vk_pipeline.c` (blend, compare and stencil mappings, vertex format table with its 3-component fallback and pipeline cache file of `runtime/nvn/vulkan/exec.c` and `device.c`), `runtime/vk_exec.c` (pass begin and end, scissor clip and attachment clears of `runtime/nvn/vulkan/exec.c`), `runtime/vk_exec_xfer.c` (copies of `runtime/nvn/vulkan/exec.c`), `runtime/vk_exec_draw.c` (uniform buffer spans, texture and sampler descriptors and dispatch flow of `runtime/nvn/vulkan/exec.c`); in-game menu: `runtime/ui/menu.cpp` (Dear ImGui Vulkan set-up of `runtime/nvn/vulkan/imgui_glue.cpp`) |
| MacNativeGames TotK host runtime: dynamic linking, identity check, module permissions, guest memory and SVCs, threads, IPC, audio renderer, system services, RomFS and save storage, boot sequence, the NVN proc name list | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/runtime/`, `games/totk/scripts/prepare_runtime.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/loader.c`, `runtime/memory.c`; `runtime/kernel.c`, `runtime/svc.c`, `runtime/ipc.c`, `runtime/ipc.h`, `runtime/audren.c`, `runtime/audren.h`, `runtime/audio_mixer.c`, `runtime/audio_effects.c`, `runtime/audio_effects.h`, `runtime/audio_reverb.c`, `runtime/audio_reverb.h` (SDL3 output replaces CoreAudio), `runtime/ov_system.c` (also `shared_font.c`: the BFTTF layout behind nn::pl), `runtime/ov_input.c` (also `hid_feedback.c`: vibration and six-axis handles and calls), `runtime/motion_fusion.c` (six-axis fusion of `motion_fusion.c`), `runtime/ov_media.c` (hardware Opus decoder over libopus), `runtime/swkbd_win.c` (`swkbd_host.c`: ShowKeyboardArg offsets, UTF-8/UTF-16 conversion, disabled-key filter, result codes; Win32 window replaces the macOS text bar), `runtime/ov_gfx.c`, `runtime/romfs.c`, `runtime/romfs_path.h`, `runtime/fs_host.c`, `runtime/fs.h`, `runtime/boot.c`; `runtime/gfx_window.c`; `tools/gen_nvn_procs.py`, `runtime/nvn_procs.inc` (`NVN/names.txt` and the `NVN_EXTRA` names of `totk_nvn_ext.c`) |
| MacNativeGames TotK frame-rate patches: 60 fps present-interval controller, real-time frame step and its present sampling, hair and cloth clock, trampoline slow-motion check (instruction words and 1.2.1 sites), player position chain; quick-menu capture background RomFS overlay | https://github.com/ly05010419/MacNativeGames @ `ebe621de99bfc48f3c42f2d2aa73e04fbcc6eae2` (`games/totk/runtime/game_patches.c`, `game_version.h`, `world_survey.c`, `games/totk/scripts/make_romfs_patches.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/frame_rate.c`, `recomp/patches-v121.txt`, `tools/make_romfs_patches.py` |
| MacNativeGames TotK 1.4.3 fade fix: the four distance / LOD and pre-actor fade sites, their instruction words and the floor of the fade step at 1 | https://github.com/ly05010419/MacNativeGames @ `3e9e7f67fdfdd3404134dcfa0f24969a9425a33f` (`games/totk/runtime/game_patches_v143.c` patch_fade_increment), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set fade; the floor without flags, clz >> 5, as in pull request #78) |
| MacNativeGames TotK aspect ratio: the camera and viewport sites that hand the perspective projection its aspect ratio, and the UI layout edits for a canvas wider than 16:9 | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/runtime/aspect_patch.h`, `aspect_math.h`, `game_patches_v143.c`, `games/totk/scripts/ultrawide_layout.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set aspect), `tools/ultrawide_layout.py`, `tools/make_romfs_patches.py` (--aspect) |
| TOTK-Ultrawide, the community mod for emulators: the list of UI panes the ultrawide layout edits and the distances they move (data only, no code copied; the edit code is our own, and MacNativeGames' `ultrawide_layout.py` in the row above already carries the same values). The mod's repository URL and licence version are not recorded in this repository | TOTK-Ultrawide (community mod; URL not recorded here) | GPL (version not recorded here) | `tools/ultrawide_layout.py` (`KEEP_ROOT`, `edits`, `anim_edits`) |
| MacNativeGames TotK virtual-core preemption: asynchronous preemption of an owner in top-level game code, priority inheritance through guest mutexes, the SDK's preemption-disable protocol (TLR+0x100/+0x102, SVC 0x36), the core-wait diagnostics | https://github.com/ly05010419/MacNativeGames @ `33e8fa83052ea1ae09a3583137008e9797e12950` (`games/totk/runtime/threads.c`, commits 8bf0893 and 19730cd), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/kernel.c` (SuspendThread and a register-saving stub replace SIGUSR2), `runtime/hostfn.c`, `runtime/svc.c` (host depth) |
| MacNativeGames RomFS index builder (`TOTKRFS1` layout) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/scripts/prepare_romfs_backend.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/romfs_index.py` |
| d2r-mobile host thread environment (TLR, TLS, thread object), the origin of MacNativeGames' thread layer; d2r-mobile NVN host object layouts, command record format and NVN front end, as adapted for TotK in MacNativeGames' `d2r_gfx` copy | https://github.com/jhaizhou-ops/d2r-mobile @ `53e7c5bb9ca3ea53e2160005be8fbe920cd9cb53` (`dm/host/thread.c`, `dm/host/hle_thread.c`); the NVN layouts and front end via https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/third_party/d2r_gfx/gfx/nvn/nvn_obj.h`, the two types of `nvn_tex.h`, `hle_nvn.c`, `totk_nvn_ext.c`, `nvn_impl.h`, `nvn_hmap.h`, `nvn_pool_index.h`, the request checks and mapping semantics of `totk_vpool.c`, the generation stamps of `nvn_dirty.c`, texture mirroring and texel copies of `nvn_mtl.m`; licence in `games/totk/third_party/d2r_gfx/LICENSE`), Copyright (c) 2026 jhaizhou | MIT | `runtime/kernel.c`; `runtime/nvn_obj.h`; `runtime/nvn_api.h`, `runtime/nvn_api_core.c`, `runtime/nvn_hmap.h`, `runtime/nvn_pool_index.h`; `runtime/nvn_api_obj.c` (also `nvn_pace.h`); `runtime/nvn_api_cmd.c`, `runtime/nvn_ext.c`; `runtime/vk_memory.c`, `runtime/vk_texture.c`; `runtime/vk_sync.c` (side effects written after GPU completion, `hle_nvn.c`); `runtime/vk_pipeline.c` (pipeline key and state mapping of `nvn_mtl.m` `pso_key_fill` / `pso_desc`, its `vfmt` vertex formats and the missing-varying fix-up of `msl_varyings`); `runtime/vk_exec.c`, `runtime/vk_exec.h` (op loop, pure-state ops and state defaults of `nvn_mtl.m` `mtl_exec` / `state_op` / `state_reset`); `runtime/vk_exec_xfer.c` (copy, clear, event, counter and conditional-render semantics of `nvn_mtl.m` `copy_t2b` / `copy_b2t` / `copy_t2t` / `clear_tex` / `cond_eval`); `runtime/vk_exec_draw.c` (bind ops, draw and dispatch flow, vertex streams of `nvn_mtl.m` `state_op` / `bind_vertex_streams` / `draw_inner` / `dispatch`) |
+1 -1
View File
@@ -1 +1 @@
0.1.6
0.1.9
+9 -2
View File
@@ -82,7 +82,7 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
- Vulkan 1.3 back end in place of MacNativeGames' Metal one. At `QueueSubmitCommands` the recorded NVN commands are replayed into Vulkan command buffers (`vk_exec*.c`).
- Memory pools. Apple's unified memory let MacNativeGames use the game's pools zero-copy. The back end imports the pools as host memory (`VK_EXT_external_memory_host`). On a discrete GPU, pools bound as buffers are mirrored in device-local memory and the pages the CPU wrote (write watch, above) are uploaded before a replay; idle pool regions are made read-only so the write-watch queries skip them (`vk_memory.c`). GPU-rendered textures are written back when the CPU reads them: on a pool invalidate, on a CPU copy, and for small targets once the CPU waits for a fence (`vk_texture.c`).
- Replay thread. `TOTK_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is off by default because no timing has shown a gain yet.
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once, and the submit waits until the ring is empty. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. `TOTK_VK_RECORDER=0` makes every call on the submitting thread.
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. `TOTK_VK_RECORDER=0` makes every call on the submitting thread.
- Uniform snapshot. The game rewrites per-object uniform blocks on other threads soon after it submits the frame
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
binds from CPU-written pools is copied, and the draws bind the copy (`vk_usnap.c`, after MacNativeGames'
@@ -93,7 +93,14 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
- Resolution. Patch sets make the game read its output, swapchain and scene sizes from the patch page
(MacNativeGames' sites); `resolution.c` sets them from `output_res` and `render_scale` and grows the texture heap
and the application pool to match. When the window is larger than the output, `vk_fsr.c` runs AMD FSR 1 (EASU,
RCAS) on the present.
RCAS) on the present. An `output_res` that is not 16:9 (more than 1% off, `res_is_16x9`) also switches on the
`aspect` patch set (1.4.3 only), which hands the output's ratio to the camera, and `romfs.c` `overlay_init` loads the
`romfs-patches-<w>x<h>` overlay for it when it exists (`tools/make_romfs_patches.py --aspect`, laid out by
`tools/ultrawide_layout.py`; the installer builds four sizes: 2560x1080, 3440x1440, 3840x1600, 5120x2160). Other
shapes, such as 16:10 or 32:9, get the wide camera and the 16:9 HUD stretched over it. 1.2.1 has no camera patch: a
wide size there keeps the 16:9 view. The `aspect` setting (`fit`, the default; `stretch`) decides what the present does
when the window's shape differs from the output's: `fit` letterboxes, where a 16:9 game in a wider window used to be
stretched.
- Occlusion queries never block the CPU; a result that is not ready counts as visible.
- ASTC textures are uploaded as they are where the device reports `textureCompressionASTC_LDR`, and decoded by a compute shader everywhere else (`vk_astc.c`, `TOTK_ASTC`). The CPU decoder (`tex_astc*.c`) is the fallback and the test oracle.
- Draw state. A draw whose program, bound state and textures repeat reuses the previous draw's shaders and skips rebuilding its descriptor sets; descriptor sets are written through update templates (`vk_exec_draw.c`, `vk_pipeline.c`).
+3
View File
@@ -89,6 +89,9 @@ From the repository root, in PowerShell. Every step that depends on the game ver
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
`--patches` builds in the frame-rate, resolution and other patch sets, switchable at run time. The link copies
`SDL3.dll`, `libiconv-2.dll` and `libopus-0.dll` next to the exe.
For Xbox button prompts, run `make_romfs_patches.py` once more with `--xbox-prompts`: it writes
`build\<ver>\romfs-patches-xbox` ([controls.md](controls.md), "Xbox button prompts"); `build_game.py` copies it next
to `totk.exe` like `romfs-patches`.
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
for the shared shader list:
+20 -2
View File
@@ -7,8 +7,9 @@ Input comes from the first SDL3 gamepad, the keyboard and the mouse, merged into
- `nintendo`: by position, as on a Switch (the table below).
- `xbox`: south A, east B, west X, north Y, as an Xbox pad labels them; on a PlayStation pad cross is A, circle B, square X, triangle Y.
- `auto` (default): `nintendo` for a pad whose east button SDL reports labelled A (Switch Pro Controller, Nintendo-layout pads), `xbox` for the others.
- `xbox_prompts`: the buttons as `nintendo`, and the game's prompts name the Xbox labels (see Xbox button prompts below).
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. The game's button prompts stay Switch letters either way.
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. With `nintendo`, `xbox` and `auto` the game's button prompts stay Switch letters; `xbox_prompts` changes them.
| Switch | Gamepad (by position, Nintendo layout) | Keyboard and mouse | Action in the world |
|---|---|---|---|
@@ -30,6 +31,19 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
## Xbox button prompts
`pad_layout = xbox_prompts` (*Xbox prompts (A right)* on the Controls tab) is for an Xbox pad played by position. The buttons work as with `nintendo`: the right button, labelled B, does Switch A (interact, confirm), and the bottom one, labelled A, does Switch B (run, cancel). The letters the game shows are swapped to match the labels, A with B and X with Y, so each prompt names the button to press. The other layouts leave the prompts alone: with `xbox` the Switch letters already match the labels.
The swapped letters are part of the game's textures, fonts and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the letter the game draws for it, and the prompt then names the wrong button. Use `xbox_prompts` with the default bindings.
The buttons change at once, but the game reads its UI archive and fonts once at boot, so the prompts change at the next start (the menu says *prompts change after restart*). `xbox_prompts` loads `romfs-patches-xbox` next to `romfs-index.bin` in place of `romfs-patches` (`runtime/romfs.c`, `overlay_init`): the same 60 fps patches, with the swap made from the game's own files by `tools/make_romfs_patches.py --xbox-prompts`, which the setup runs next to the plain one:
- the action prompt (`DoCommand_00`) draws the letter images `Nt_KeyTexA/B/X/Y_00`, whose image data is swapped, in a diamond that names the other face buttons. The diamond's letters are text the game takes from `LayoutMsg/DoCommand_00.msbt` of each language's `Mals` archive (and the layout's own text panes behind it); both are swapped. The diamond's shape shows the button's position and stays.
- every other prompt is text in the button font (`scft/External.bfotf` in each `Font/Font*.Nin_NX_NVN.bfarc.zs`, glyphs U+E040 to U+E043); the A and B, X and Y glyphs are swapped.
Without the folder (an install made by an older setup, or one where the setup could not prepare it and showed a warning) the menu says the files are missing, the log has `RomFS overlay: pad_layout = xbox_prompts, but ...`, and the Switch prompts stay; running the setup again on the install folder writes it. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --xbox-prompts` writes `build\<ver>\romfs-patches-xbox`.
## Window keys
| Key | Gamepad | Action |
@@ -46,7 +60,7 @@ The *Controls* tab of the settings menu lists, under the button layout, every Sw
The tab writes `totk.ini` lines, one per Switch button or stick direction: `bind_<button> = <entry>, <entry>, ...`. The button is `A B X Y L R ZL ZR PLUS MINUS LEFT UP RIGHT DOWN LCLICK RCLICK`, or a stick direction `LSTICK_LEFT LSTICK_UP LSTICK_RIGHT LSTICK_DOWN` (movement, W A S D by default) and `RSTICK_...` (camera, the arrow keys). An entry is `pad:<name>` or `key:<name>`:
- pad names: `south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down` (`zl`, `zr` are the triggers)
- pad names: `south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down lpaddle1 rpaddle1 lpaddle2 rpaddle2` (`zl`, `zr` are the triggers; the paddles are the back buttons of an Xbox Elite, DualSense Edge or Steam Controller, upper pair 1, lower pair 2, and no Switch button has them by default)
- key names: `a` to `z`, `0` to `9`, `f2` to `f7`, `f9`, `f12`, `num0` to `num9`, `space tab enter backspace lshift rshift lctrl rctrl lalt capslock insert delete home end pageup pagedown up down left right`, `num_add num_sub num_mul num_div num_decimal`, `minus equals lbracket rbracket semicolon apostrophe comma period slash backslash grave`, `mouse_l mouse_r mouse_m mouse_x1 mouse_x2`
- `key:default` and `pad:default` keep that part's defaults (`pad:default` is each layout's own button).
- `none` leaves the button unbound.
@@ -68,6 +82,8 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
`TOTK_MOUSE_SENS` (0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `TOTK_INPUT=none` disables the capture too.
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it.
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
## Scripted input
@@ -85,6 +101,8 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
`TOTK_INPUT_RECORD=<file>` (`input_record` in `totk.ini`, or Input recording in the menu) writes the merged input in the same format, one line per button press and per stick segment: events before the world clock starts by tick, the rest timed; a button or stick held when the world clock starts is cut there and goes on as a timed event. The file replays as a script, which makes it the repro to attach to a bug report.
`TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry>` attaches an SDL virtual gamepad at start whose sticks rest at these raw SDL axis values (-32768 to 32767, down is +y), so a drifting or held stick can be tested without a controller: the values go through the gamepad mapping and `TOTK_STICK_DEADZONE` like a real pad's, and `TOTK_INPUT_RECORD` shows what reached the game.
## RenderDoc capture (blocked)
`TOTK_RDOC=<frame>[,...]` (`runtime/rdoc_win.c`; `TOTK_RDOC_DIR`, `TOTK_RDOC_DLL`) loads RenderDoc before `vkCreateInstance`. RenderDoc hides `VK_EXT_external_memory_host`, which the back end requires for guest pool imports (`runtime/vk_memory.c`), so device creation fails under it. A capture needs a copy-based pool fallback first.
+6 -2
View File
@@ -24,7 +24,7 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
exefs/, romfs/ extracted from the player's files
profile/ saves
logs/ totk.log of the last run, totk.1.log to totk.4.log of the runs before it
shader and pipeline caches prepared by the setup
shader and pipeline caches prepared by the setup; shader-cache/shader-cache.pack holds the translations
totk-install.json what built this folder: release version, input hashes, finished steps
```
@@ -50,7 +50,11 @@ keeps `profile/`. Saves carry over both ways: 1.4.3 converts a 1.2.1 save on loa
hashes and the game version's patch page) and link
them with the prebuilt runtime library into `totk.exe` (`build_game.py --runtime-lib`; about 3 min on 24 cores).
5. **Prepare** the RomFS patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
`totk.exe --prepare-shaders`) in the install folder.
`totk.exe --prepare-shaders`) in the install folder, then fold the shader cache's loose files (four per shader, about
117,000) into `shader-cache/shader-cache.pack` (`tools/pack_shader_cache.py`). totk.exe reads the pack first and
writes the translations it makes in play as loose files; running the tool again with the game closed folds those in.
The patches are written twice: `romfs-patches` and `romfs-patches-xbox` (`--xbox-prompts`, the A/B and X/Y prompts
swapped for `pad_layout = xbox_prompts`).
6. **Clean up** the temp folder (generated C and objects, about 1.3 GB).
A cancelled or failed setup resumes from the last finished step (`totk-install.json`).
+3 -1
View File
@@ -72,7 +72,9 @@ class Layout:
def missing(self):
"""Paths the steps need that do not exist."""
need = [self.python, self.tools / 'extract.py', self.tools / 'build_game.py', self.tools / 'prepare_shaders.py',
self.tools / 'make_romfs_patches.py', self.recomp / 'gen.py', self.clang,
self.tools / 'pack_shader_cache.py', self.tools / 'make_romfs_patches.py', self.tools / 'ultrawide_layout.py',
self.recomp / 'gen.py',
self.clang,
self.deps / 'include', self.deps / 'lib', self.runtime, self.runtime_lib, self.ninja, self.hactool,
self.xlate, *self.dlls, *(f(v) for v in SUPPORTED for f in (self.patches, self.expected))]
if self.shader_list:
+51 -2
View File
@@ -6,7 +6,8 @@ tools/prepare_shaders.py, whose output goes to <temp>/logs/<step>-<tool>.log and
here: the host check is a small C program built with the bundled clang (hostcheck.c).
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, totk.exe and its DLLs, romfs-index.bin,
romfs-patches/, totk_xlate.exe, the shader state of totk.exe --prepare-shaders, totk.ini, totk-install.json.
romfs-patches/, romfs-patches-xbox/, totk_xlate.exe, the shader state of totk.exe --prepare-shaders, totk.ini,
totk-install.json.
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), gen/ (C), build/ (objects),
logs/ (kept by the cleanup).
"""
@@ -581,13 +582,17 @@ class Compile(Step):
TRANSLATED = re.compile(r'gfx_shader: prepare: (\d+) of (\d+) pairs')
WIDE_SIZES = ('2560x1080', '3440x1440', '3840x1600', '5120x2160') # runtime/ui/menu.cpp row_output_res
class Prepare(Step):
id = 'prepare'
def key(self, ctx):
layout = ctx.layout
xlate_id = layout.xlate.with_suffix('.id')
tools = [layout.tools / n for n in ('prepare_shaders.py', 'shader_census.py', 'make_romfs_patches.py')]
tools = [layout.tools / n for n in ('prepare_shaders.py', 'shader_census.py', 'make_romfs_patches.py',
'pack_shader_cache.py', 'ultrawide_layout.py')]
return fingerprint('prepare-1', ctx.keys['compile'], layout.xlate, xlate_id, shader_list_text(layout), *tools)
def current(self, ctx, record):
@@ -617,6 +622,28 @@ class Prepare(Step):
dest / 'romfs-patches'])
if code:
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
# The same overlay with the UI laid out for the 21:9 game resolutions the settings menu offers (runtime/romfs.c
# overlay_init picks the one that matches). Only 1.4.3 has the camera aspect patch (recomp/patches-v143.txt) the
# layouts go with, and ultrawide_layout.py is checked against 1.4.3's layout archive: 1.2.1 builds none.
for size in WIDE_SIZES:
remove(dest / f'romfs-patches-{size}')
if version_tag(game_version(ctx)) != 'v143':
continue
code, log = run_tool(ctx, 'prepare', layout.tools / 'make_romfs_patches.py',
['--version', 'v143', '--romfs', dest / 'romfs', '--out',
dest / f'romfs-patches-{size}', '--aspect', size])
if code:
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
# romfs-patches-xbox: the same with the A/B and X/Y prompts swapped, for pad_layout = xbox_prompts (written on
# every install, so the menu choice works without another run of the setup). Optional: when it fails, the game
# keeps the Switch prompts and the menu says the files are missing, so the setup warns and goes on.
remove(dest / 'romfs-patches-xbox')
code, log = run_tool(ctx, 'prepare-xbox', layout.tools / 'make_romfs_patches.py',
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
dest / 'romfs-patches-xbox', '--xbox-prompts'])
if code:
remove(dest / 'romfs-patches-xbox')
rep.warning('warn.xbox_prompts_failed', code=code, log=str(log))
shader_list = layout.shader_list
if not shader_list:
@@ -638,6 +665,12 @@ class Prepare(Step):
dest / 'exefs', '--list', shader_list], parse)
if code or not (dest / 'shader-prepared.bin').is_file():
raise InstallError('prepare.shaders_failed', code=code, log=str(log))
# One shader-cache.pack instead of four files per shader (~117,000): totk.exe reads the pack first.
rep.progress('prepare', 'pack', force=True)
code, log = run_tool(ctx, 'prepare', layout.tools / 'pack_shader_cache.py', [dest / 'shader-cache'])
if code:
raise InstallError('prepare.pack_failed', code=code, log=str(log))
return {'xlate_sha256': file_sha256(xlate)}
@@ -661,8 +694,24 @@ class Cleanup(Step):
remove(stale)
for name in ('gen', 'build', 'check', 'shader_list.tsv'):
remove(ctx.temp / name)
romfs_problem(dest / 'romfs', expected_update(ctx))
return {}
def romfs_problem(romfs, expected):
"""Raise cleanup.romfs_unreadable unless romfs/ still holds the verified image. The game opens romfs.bin at start;
an install whose romfs/ went empty and unreadable after the extraction verified it (a folder Windows was still
deleting, #107) reported success and the game stopped at its first file read."""
try:
ok = extract.romfs_verified(romfs, expected) and (romfs / 'romfs.bin').stat().st_size == expected['romfs']['size']
if ok:
with open(romfs / 'romfs.bin', 'rb') as f:
f.read(1)
except (OSError, ValueError):
ok = False
if not ok:
raise InstallError('cleanup.romfs_unreadable', path=str(romfs))
def all_steps():
return [Check(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
+4
View File
@@ -26,6 +26,7 @@
"phase.prepare.seed": "Reading the shaders from the game data",
"phase.prepare.translate": "Translating the shaders",
"phase.prepare.libraries": "Building the shaders for the graphics driver",
"phase.prepare.pack": "Packing the shader cache into one file",
"phase.cleanup.ini": "Writing totk.ini",
"phase.cleanup.temp": "Removing temporary files",
@@ -75,10 +76,13 @@
"prepare.shader_list_missing": "The shader list is missing.",
"prepare.romfs_patches_failed": "Writing the game data patches failed (exit code {code}). Details: {log}",
"prepare.shaders_failed": "Preparing the shaders failed (exit code {code}). Details: {log}",
"cleanup.romfs_unreadable": "The game data in {path} is missing or cannot be read after the install. Restart Windows, then run the installer again on the same folder: it extracts the game data again and keeps the rest.",
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
"cleanup.ini_failed": "totk.ini cannot be written: {detail}",
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, which the installer does not extract; some text may not show.",
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
"ui.title": "TotK Setup",
"ui.version": "version {version}",
+4
View File
@@ -26,6 +26,7 @@
"phase.prepare.seed": "Чтение шейдеров из данных игры",
"phase.prepare.translate": "Трансляция шейдеров",
"phase.prepare.libraries": "Сборка шейдеров для драйвера видеокарты",
"phase.prepare.pack": "Упаковка кэша шейдеров в один файл",
"phase.cleanup.ini": "Запись totk.ini",
"phase.cleanup.temp": "Удаление временных файлов",
@@ -75,10 +76,13 @@
"prepare.shader_list_missing": "Нет списка шейдеров.",
"prepare.romfs_patches_failed": "Не удалось записать патчи данных игры (код выхода {code}). Подробности: {log}",
"prepare.shaders_failed": "Не удалось подготовить шейдеры (код выхода {code}). Подробности: {log}",
"cleanup.romfs_unreadable": "Данные игры в {path} пропали или не читаются после установки. Перезагрузите Windows и запустите установщик снова на ту же папку: он заново распакует данные игры, остальное сохранится.",
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
"cleanup.ini_failed": "Не удаётся записать totk.ini: {detail}",
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, которые установка не извлекает; часть текста может не отображаться.",
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
"ui.title": "Установка TotK",
"ui.version": "версия {version}",
+12
View File
@@ -73,6 +73,18 @@ site scene main 585e50 bd0cc660 2d420460 # str s0, [x19, #0xcc4] -> ldp s0, s
site scene main 585e54 1e220140 bd0cc660 # scvtf s0, w10 -> str s0, [x19, #0xcc4]
site scene main 585e60 bd0cca60 bd0cca61 # str s0, [x19, #0xcc8] -> str s1, [x19, #0xcc8]
# Camera aspect ratio for a game resolution that is not 16:9 (upstream game_patches_v143.c and aspect_patch.h
# aspect_install / aspect_stub; runtime/resolution.c res_init writes the output's width / height to page +56 and
# switches the set on). Each site leaves the ratio sead's perspective projection (+0xb0) takes in s0: two camera sites
# (update and setup) load the camera's copy of DefaultAspect 1.777778, two viewport routines divide width by height.
# The replacement loads the ratio from the page instead, so a 21:9 output shows more of the world at the sides with
# the vertical view kept (Hor+). x10 is kept on the stack, so no register liveness is assumed.
set aspect TOTK_ASPECT_PATCH 0
site aspect main 628c74 bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x19, #0x140] (camera update)
site aspect main b43460 bd406ac0 f81f0fea @page:x10 bd403940 f84107ea # ldr s0, [x22, #0x68] (camera setup)
site aspect main 1ff04f4 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
site aspect main 253fbb0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
# its wrapper ZSTD_decompress_usingDDict (main+0x3a1e0, which tail-branches to it) goes to a host function on the patch
+147 -7
View File
@@ -3,8 +3,10 @@
// HDR void extents, random blocks kept by what they exercise (1-4 partitions, dual plane with 1-3 partitions, every
// weight grid and quantization the random modes reach), then plain random blocks (mostly error blocks, HDR endpoint
// modes among them). Each case decodes two regions into two mip levels of an RGBA8 image with cropped edges, and
// compares both the decoder's texels (readback) and what the image holds afterwards. Also times one 2048x2048 texture
// on the CPU (astc_decode_ldr_mt) and on the GPU (wall time of the upload batch).
// compares both the decoder's texels (readback) and what the image holds afterwards. The same regions also go through
// the BC3 encoder (TOTK_ASTC_BC) into a BC3 image, whose blocks are decoded here and held to error bounds (bc_check).
// Also times one 2048x2048 texture on the CPU (astc_decode_ldr_mt) and on the GPU (wall time of the upload batch).
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -26,7 +28,8 @@ static const uint8_t dims[14][2] = {{4, 4}, {5, 4}, {5, 5}, {6, 5}, {6, 6}, {8,
// Buckets of valid blocks: partitions 1-4 single plane, then dual plane with 1-3 partitions.
enum { NB = 7, PER_BUCKET = 48 };
static const char *const bucket_name[NB] = {"1p", "2p", "3p", "4p", "dual 1p", "dual 2p", "dual 3p"};
static uint64_t g_bucket[NB], g_void, g_err, g_blocks, g_texels;
static uint64_t g_bucket[NB], g_void, g_err, g_blocks, g_texels, g_bc_cases;
static double g_bc_db = 99; // the lowest BC3 PSNR of all cases
static int dual_bit(uint32_t m) {
if (!(m & 3) && (m >> 7 & 3) == 2) return 0; // the 6..10 x 6..10 modes have no dual plane
@@ -77,9 +80,9 @@ typedef struct {
VmaAllocation alloc;
} Img;
static int make_image(VkCtx *c, uint32_t w, uint32_t h, uint32_t levels, int srgb, Img *o) {
static int make_image(VkCtx *c, uint32_t w, uint32_t h, uint32_t levels, VkFormat format, Img *o) {
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
.format = srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, .extent = {w, h, 1},
.format = format, .extent = {w, h, 1},
.mipLevels = levels, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
@@ -120,7 +123,7 @@ static double now_ms(void) {
static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32_t w0, uint32_t h0, uint32_t w1,
uint32_t h1, uint32_t bw, uint32_t bh, int srgb, uint8_t *out_rb, uint8_t *out_img, double *ms) {
Img im;
if (!make_image(c, w0, h0, 2, srgb, &im)) return 0;
if (!make_image(c, w0, h0, 2, srgb ? VK_FORMAT_R8G8B8A8_SRGB : VK_FORMAT_R8G8B8A8_UNORM, &im)) return 0;
size_t t0 = (size_t)w0 * h0 * 4, t1 = (size_t)w1 * h1 * 4;
VkCommandBuffer cb = vk_upload_begin();
VkBuffer sb, rb, ib;
@@ -132,7 +135,7 @@ static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32
VkAstcRegion ar[2] = {{.src = 0, .w = w0, .h = h0, .slices = 1, .level = 0},
{.src = n0 * 16, .w = w1, .h = h1, .slices = 1, .level = 1}};
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, im.img, VK_IMAGE_ASPECT_COLOR_BIT, rb);
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, 0, im.img, VK_IMAGE_ASPECT_COLOR_BIT, rb);
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
VkBufferImageCopy cp[2] = {{.bufferOffset = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w0, h0, 1}},
{.bufferOffset = t0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 1, 0, 1}, .imageExtent = {w1, h1, 1}}};
@@ -152,6 +155,123 @@ static int gpu_decode(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32
return ok;
}
// The same two regions decoded and encoded to BC3 (TOTK_ASTC_BC) into a BC3 image; out: its blocks read back, level 0
// then level 1, rows of 4x4 blocks. 0 on a Vulkan failure or when nothing was recorded.
static int gpu_bc(VkCtx *c, const uint8_t *blk, size_t n0, size_t n1, uint32_t w0, uint32_t h0, uint32_t w1, uint32_t h1,
uint32_t bw, uint32_t bh, int srgb, uint8_t *out) {
Img im;
if (!make_image(c, w0, h0, 2, srgb ? VK_FORMAT_BC3_SRGB_BLOCK : VK_FORMAT_BC3_UNORM_BLOCK, &im)) return 0;
size_t b0 = (size_t)((w0 + 3) / 4) * ((h0 + 3) / 4) * 16, b1 = (size_t)((w1 + 3) / 4) * ((h1 + 3) / 4) * 16;
VkCommandBuffer cb = vk_upload_begin();
VkBuffer sb, ib;
VmaAllocation ia;
uint8_t *stg = vk_staging((n0 + n1) * 16, &sb, NULL), *imap = vk_staging(b0 + b1, &ib, &ia);
memcpy(stg, blk, (n0 + n1) * 16);
to_general(cb, im.img);
VkAstcRegion ar[2] = {{.src = 0, .w = w0, .h = h0, .slices = 1, .level = 0},
{.src = n0 * 16, .w = w1, .h = h1, .slices = 1, .level = 1}};
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
int ok = vk_astc_record(cb, sb, (n0 + n1) * 16, ar, 2, bw, bh, srgb, 1, im.img, VK_IMAGE_ASPECT_COLOR_BIT, VK_NULL_HANDLE);
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, ALL_RW);
VkBufferImageCopy cp[2] = {{.bufferOffset = 0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .imageExtent = {w0, h0, 1}},
{.bufferOffset = b0, .imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 1, 0, 1}, .imageExtent = {w1, h1, 1}}};
vkCmdCopyImageToBuffer(cb, im.img, VK_IMAGE_LAYOUT_GENERAL, ib, 2, cp);
barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_HOST_BIT, VK_ACCESS_2_HOST_READ_BIT);
vk_upload_end();
vk_upload_flush(1);
vmaInvalidateAllocation(c->vma, ia, 0, VK_WHOLE_SIZE);
memcpy(out, imap, b0 + b1);
vmaDestroyBuffer(c->vma, ib, ia);
vk_defer_image(im.img, im.alloc);
vk_upload_flush(1);
return ok;
}
// One BC3 block to 16 RGBA8 texels, as the format defines it (565 endpoints widened by bit replication; the colour
// block always in four-colour mode). pos: each texel's place on the line from endpoint 0 (0) to endpoint 1 (3).
static void bc3_decode(const uint8_t b[16], uint8_t t[16][4], uint8_t ep[2][3], int pos[16]) {
uint8_t a[8] = {b[0], b[1]}, p[4][3];
if (a[0] > a[1])
for (int i = 1; i < 7; i++) a[i + 1] = (uint8_t)(((7 - i) * a[0] + i * a[1] + 3) / 7);
else {
for (int i = 1; i < 5; i++) a[i + 1] = (uint8_t)(((5 - i) * a[0] + i * a[1] + 2) / 5);
a[6] = 0, a[7] = 255;
}
uint64_t ai = 0;
for (int i = 0; i < 6; i++) ai |= (uint64_t)b[2 + i] << 8 * i;
for (int k = 0; k < 2; k++) {
unsigned v = b[8 + 2 * k] | b[9 + 2 * k] << 8, r = v >> 11, g = v >> 5 & 63, bl = v & 31;
p[k][0] = (uint8_t)(r << 3 | r >> 2), p[k][1] = (uint8_t)(g << 2 | g >> 4), p[k][2] = (uint8_t)(bl << 3 | bl >> 2);
}
for (int ch = 0; ch < 3; ch++)
p[2][ch] = (uint8_t)((2 * p[0][ch] + p[1][ch] + 1) / 3), p[3][ch] = (uint8_t)((p[0][ch] + 2 * p[1][ch] + 1) / 3);
uint32_t ci = b[12] | b[13] << 8 | b[14] << 16 | (uint32_t)b[15] << 24;
static const int code_pos[4] = {0, 3, 1, 2};
for (int i = 0; i < 16; i++) {
memcpy(t[i], p[ci >> 2 * i & 3], 3);
t[i][3] = a[ai >> 3 * i & 7];
pos[i] = code_pos[ci >> 2 * i & 3];
}
memcpy(ep, p, 6);
}
// BC3 is lossy, so these are bounds, not bit-exactness. In every block: alpha within a fourteenth of the block's alpha
// range (the eight-value mode's half step); each texel on the palette entry nearest its projection onto the endpoint
// line (within rounding, when the endpoints are apart); a block of one colour within the 565 rounding, alpha exact.
// Per level, a PSNR floor against astc_decode_ldr for gross damage: the random blocks are noise, so it is low (19 dB
// at worst for 5x5 level 1 on an RX 9070). *db: the level's PSNR.
enum { BC_MIN_DB = 15 };
static int bc_check(const uint8_t *want, const uint8_t *blocks, uint32_t w, uint32_t h, const char *what, double *db) {
uint32_t bx = (w + 3) / 4, by = (h + 3) / 4;
double se = 0;
for (uint32_t y4 = 0; y4 < by; y4++)
for (uint32_t x4 = 0; x4 < bx; x4++) {
uint8_t t[16][4], ep[2][3], lo[4] = {255, 255, 255, 255}, hi[4] = {0};
int pos[16];
bc3_decode(blocks + 16 * ((size_t)y4 * bx + x4), t, ep, pos);
double axis[3], len2 = 0;
for (int ch = 0; ch < 3; ch++) axis[ch] = (double)ep[1][ch] - ep[0][ch], len2 += axis[ch] * axis[ch];
for (int pass = 0; pass < 2; pass++)
for (int i = 0; i < 16; i++) {
uint32_t x = x4 * 4 + (i & 3), y = y4 * 4 + (i >> 2);
if (x >= w || y >= h) continue;
const uint8_t *s = want + 4 * ((size_t)y * w + x);
if (!pass) {
for (int ch = 0; ch < 4; ch++) lo[ch] = s[ch] < lo[ch] ? s[ch] : lo[ch], hi[ch] = s[ch] > hi[ch] ? s[ch] : hi[ch];
continue;
}
int flat = !memcmp(lo, hi, 4), lim[4] = {5, 3, 5, 0};
if (len2 >= 64) { // shorter lines: rounding alone can move a texel to the next entry
double proj = 0;
for (int ch = 0; ch < 3; ch++) proj += ((double)s[ch] - ep[0][ch]) * axis[ch];
proj = proj / len2 * 3;
proj = proj < 0 ? 0 : proj > 3 ? 3 : proj;
if (fabs(pos[i] - proj) > 0.75) { // a wrong code mapping is a whole place off
printf("probe-astc: FAIL BC3 %s: texel (%u,%u) on palette place %d, its projection is %.2f\n",
what, x, y, pos[i], proj);
return 1;
}
}
for (int ch = 0; ch < 4; ch++) {
int d = abs((int)t[i][ch] - s[ch]);
se += (double)d * d;
if ((ch == 3 && d > (hi[3] - lo[3]) / 14 + 1) || (flat && d > lim[ch])) {
printf("probe-astc: FAIL BC3 %s: texel (%u,%u) channel %d: astc %02x%02x%02x%02x bc3 %02x%02x%02x%02x\n",
what, x, y, ch, s[0], s[1], s[2], s[3], t[i][0], t[i][1], t[i][2], t[i][3]);
return 1;
}
}
}
}
double mse = se / ((double)w * h * 4);
*db = mse > 0 ? 10 * log10(255.0 * 255.0 / mse) : 99;
if (*db < BC_MIN_DB) {
printf("probe-astc: FAIL BC3 %s: PSNR %.1f dB, below %d\n", what, *db, BC_MIN_DB);
return 1;
}
return 0;
}
static int first_diff(const uint8_t *a, const uint8_t *b, size_t texels, size_t *at) {
for (size_t i = 0; i < texels; i++)
if (memcmp(a + 4 * i, b + 4 * i, 4)) { *at = i; return 1; }
@@ -189,6 +309,22 @@ static int test_case(VkCtx *c, uint32_t bw, uint32_t bh, int srgb) {
}
}
}
if (!bad && vk_astc_bc()) {
size_t b0 = (size_t)((w0 + 3) / 4) * ((h0 + 3) / 4) * 16, b1 = (size_t)((w1 + 3) / 4) * ((h1 + 3) / 4) * 16;
uint8_t *bc = malloc(b0 + b1);
char what[2][48];
double db[2] = {0, 0};
for (int lv = 0; lv < 2; lv++) snprintf(what[lv], sizeof what[lv], "%ux%u %s level %d", bw, bh, srgb ? "sRGB" : "linear", lv);
if (!gpu_bc(c, blk, n0, n1, w0, h0, w1, h1, bw, bh, srgb, bc)) {
printf("probe-astc: FAIL BC3 %s: the encode was not recorded\n", what[0]);
bad = 1;
}
bad = bad || bc_check(want, bc, w0, h0, what[0], &db[0]) || bc_check(want + t0 * 4, bc + b0, w1, h1, what[1], &db[1]);
for (int lv = 0; lv < 2 && !bad; lv++)
if (db[lv] < g_bc_db) g_bc_db = db[lv];
g_bc_cases += !bad;
free(bc);
}
g_blocks += n0 + n1, g_texels += t0 + t1;
free(blk), free(want), free(rb), free(im);
return bad;
@@ -223,6 +359,7 @@ static void timing(VkCtx *c, uint32_t bw, uint32_t bh) {
}
int probe_astc(void) {
_putenv_s("TOTK_ASTC_BC", "1"); // also builds the BC3 encoder; the RGBA8 cases pass bc 0 and do not use it
if (!vk_init()) { printf("probe-astc: FAIL vk_init\n"); return 1; }
VkCtx *c = vk_ctx();
printf("probe-astc: GPU %s, textureCompressionASTC_LDR %d, mode %d\n", c->props.deviceName, c->has_astc_ldr,
@@ -236,6 +373,9 @@ int probe_astc(void) {
for (int s = 0; s < 2 && !bad; s++) bad = test_case(c, dims[i][0], dims[i][1], s);
for (int b = 0; b < NB && !bad; b++)
if (!g_bucket[b]) { printf("probe-astc: FAIL no valid %s block was generated\n", bucket_name[b]); bad = 1; }
if (!bad && !vk_astc_bc()) printf("probe-astc: BC3 encoder not available on this device: not checked\n");
else if (!bad) printf("probe-astc: BC3 encode: %llu cases within the bounds, lowest PSNR %.1f dB (floor %d)\n",
(unsigned long long)g_bc_cases, g_bc_db, BC_MIN_DB);
if (!bad) {
timing(c, 4, 4);
timing(c, 6, 6);
+50 -8
View File
@@ -9,6 +9,13 @@
// single-producer single-consumer ring. The SDL audio thread drains that ring without taking any lock: an empty ring
// plays silence and never blocks the render thread or the guest. TOTK_AUDIO=none (or TOTK_AUDIO_OFF) mixes without a
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20) is the queued audio the output waits for after start or underrun.
// The device's clock paces the render thread, as the audio hardware paces the Switch's DSP (audio_mixer_pace_ns): a
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart. LASRecomp and
// SavageWindRecomp pace their renderers by the SDL stream the same way. Without a device, while the renderer is
// stopped and while the device takes nothing (a stall), the render thread keeps real time on its own timer; what piled
// up meanwhile is skipped on the SDL thread once the device resumes (more than QUEUE_SLACK_MS above the target plus the
// device's request), as MacNativeGames' audio_mixer.c trims its ring after a device stall.
#include <math.h>
#include <stdatomic.h>
#include <stdio.h>
@@ -26,6 +33,8 @@
#define MAX_FRAME 1024
#define RING_FRAMES 16384 // stereo frames, about 340 ms at 48 kHz
#define QUEUE_SLACK_MS 40 // queued audio above the target plus the device's request that ring_pull skips
#define STALL_MS 50 // the device took nothing this long (at least 4 periods): the timer paces
#define SPLITTER_UNUSED 0xffffffffu
#define dm_guest_write_prepare(p, n) ((void)(p), (void)(n)) // guest memory is plain host memory on Windows
@@ -43,8 +52,11 @@ typedef struct Mixer {
SplitInfo *split; int nsplit; SplitDest *dest; int ndest;
SDL_AudioStream *stream; int device_ok, rate;
float ring[RING_FRAMES * 2];
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero;
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero, skipped_frames;
_Atomic int playing; int ready; // ready: the callback's own state
int target, period; // output target and device period, frames (output_open)
uint64_t stall_ns;
_Atomic uint64_t pulled_ns; // kern_now_ns of the device's last request, 0 before the first
_Atomic uint64_t flush_until; _Atomic unsigned stop_epoch; unsigned callback_epoch;
EffectState *effect; int *effect_order; unsigned effect_reported;
uint64_t *sink_address; uint32_t *sink_size, *sink_offset, *sink_old, *sink_written;
@@ -107,16 +119,23 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
atomic_store_explicit(&m->rd, wr, memory_order_release);
return;
}
uint64_t prefill = (uint64_t)m->frame * (uint64_t)m->prefill;
if (prefill < frames) prefill = frames;
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
if (!m->ready) {
uint64_t prefill = (uint64_t)m->frame * (uint64_t)m->prefill;
if (prefill < frames) prefill = frames;
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
if (have < prefill) {
memset(out, 0, (size_t)frames * 2 * sizeof(float));
return;
}
m->ready = 1;
}
uint64_t keep = (prefill > (uint64_t)m->target ? prefill : (uint64_t)m->target) + frames;
uint64_t slack = (uint64_t)m->rate * QUEUE_SLACK_MS / 1000;
if (have > keep + slack) { // piled up while the device stalled: skip the oldest, keep target + request
atomic_fetch_add(&m->skipped_frames, have - keep);
rd = wr - keep;
have = keep;
}
uint32_t n = frames < have ? frames : (uint32_t)have;
uint64_t nonzero = 0;
for (uint32_t i = 0; i < n; i++) {
@@ -139,6 +158,7 @@ static void SDLCALL sdl_pull(void *user, SDL_AudioStream *stream, int additional
(void)total;
Mixer *m = user;
float tmp[2 * 1024];
atomic_store_explicit(&m->pulled_ns, kern_now_ns(), memory_order_release);
while (additional > 0) {
uint32_t frames = (uint32_t)additional / (2 * sizeof(float));
if (!frames) break;
@@ -172,8 +192,28 @@ static void output_open(Mixer *m, int rate) {
return;
}
m->device_ok = 1;
SDL_AudioSpec dev;
int period = 0;
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(m->stream), &dev, &period) || period <= 0) period = m->frame;
m->period = period;
m->target = m->frame * m->prefill > 2 * period ? m->frame * m->prefill : 2 * period;
if (m->target > RING_FRAMES / 2) m->target = RING_FRAMES / 2;
uint64_t period_ns = (uint64_t)period * 1000000000ull / (uint64_t)rate;
m->stall_ns = 4 * period_ns > STALL_MS * 1000000ull ? 4 * period_ns : STALL_MS * 1000000ull;
const char *name = SDL_GetAudioDeviceName(SDL_GetAudioStreamDevice(m->stream));
rt_log("audio: SDL device '%s' opened, %d Hz stereo float, prefill %d ms", name ? name : "?", rate, m->prefill * 5);
rt_log("audio: SDL device '%s' opened, %d Hz stereo float, prefill %d ms, device period %d frames; the device paces "
"rendering at %.1f ms queued", name ? name : "?", rate, m->prefill * 5, period, m->target * 1000.0 / rate);
}
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
Mixer *m = r->mixer;
if (!m || !m->device_ok || !atomic_load_explicit(&m->playing, memory_order_acquire)) return -1;
uint64_t pulled = atomic_load_explicit(&m->pulled_ns, memory_order_acquire);
if (!pulled || (now > pulled && now - pulled > m->stall_ns)) return -1;
uint64_t have = atomic_load(&m->wr) - atomic_load_explicit(&m->rd, memory_order_acquire);
if (have < (uint64_t)m->target) return 0;
int64_t ns = (int64_t)((have - (uint64_t)m->target + 1) * 1000000000ull / (uint64_t)m->rate);
return ns < 500000 ? 500000 : ns; // until the device has taken it below the target
}
// Render thread only.
@@ -209,12 +249,14 @@ static void level_note(AudioRenderer *r, Mixer *m, const float *lr, int frames)
rt_log("audio: first nonzero output after %llu frames", (unsigned long long)r->elapsed_frames);
}
if (m->level_frames >= (uint64_t)r->param.sample_rate * 5) {
uint64_t wr = atomic_load(&m->wr), queued = wr - atomic_load(&m->rd);
rt_log("audio: level rms %.4f peak %.4f over %.1f s; written %llu frames, underruns %llu, overruns %llu, "
"missing frames %llu, callback frames %llu (nonzero %llu)",
"missing frames %llu, callback frames %llu (nonzero %llu), queued %.1f ms, skipped %llu frames",
sqrt(m->level_sum / (double)m->level_count), m->level_peak, m->level_frames / (double)r->param.sample_rate,
(unsigned long long)atomic_load(&m->wr), (unsigned long long)atomic_load(&m->underruns),
(unsigned long long)wr, (unsigned long long)atomic_load(&m->underruns),
(unsigned long long)atomic_load(&m->overruns), (unsigned long long)atomic_load(&m->missing_frames),
(unsigned long long)atomic_load(&m->callback_frames), (unsigned long long)atomic_load(&m->callback_nonzero));
(unsigned long long)atomic_load(&m->callback_frames), (unsigned long long)atomic_load(&m->callback_nonzero),
queued * 1000.0 / r->param.sample_rate, (unsigned long long)atomic_load(&m->skipped_frames));
m->level_sum = m->level_peak = 0;
m->level_count = m->level_frames = 0;
}
+21 -10
View File
@@ -316,7 +316,8 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
ReleaseSRWLockExclusive(&r->lock);
}
// ---- render tick: one frame every sample_count / sample_rate seconds, on a host thread
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
// or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls
static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds) {
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return;
double want = v->fraction + out_seconds * v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0);
@@ -348,6 +349,12 @@ static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds)
}
}
static void wait_ns(HANDLE timer, uint64_t ns) {
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)((ns + 99) / 100)}; // relative, 100 ns units
if (!SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE)) rt_fatal("audren: SetWaitableTimer failed (error %lu)", GetLastError());
WaitForSingleObject(timer, INFINITE);
}
static DWORD WINAPI render_thread(void *p) {
AudioRenderer *r = p;
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
@@ -358,6 +365,19 @@ static DWORD WINAPI render_thread(void *p) {
const double frame = (double)r->param.sample_count / r->param.sample_rate;
uint64_t next = kern_now_ns();
while (InterlockedCompareExchange(&r->running, 0, 0)) {
uint64_t now = kern_now_ns();
int64_t pace = audio_mixer_pace_ns(r, now);
if (pace > 0) {
wait_ns(timer, (uint64_t)pace);
continue;
}
if (pace == 0) {
next = now + frame_ns; // where the timer goes on if the device stops pacing
} else {
if (next > now) wait_ns(timer, next - now);
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
next += frame_ns;
}
AcquireSRWLockExclusive(&r->lock);
if (r->state == 0) {
voices_prepare(r);
@@ -374,15 +394,6 @@ static DWORD WINAPI render_thread(void *p) {
}
ReleaseSRWLockExclusive(&r->lock);
kern_event_signal(r->event);
next += frame_ns;
uint64_t now = kern_now_ns();
if (next > now) {
LARGE_INTEGER due = {.QuadPart = -(LONGLONG)((next - now + 99) / 100)}; // relative, 100 ns units
if (!SetWaitableTimer(timer, &due, 0, NULL, NULL, FALSE))
rt_fatal("audren: SetWaitableTimer failed (error %lu)", GetLastError());
WaitForSingleObject(timer, INFINITE);
} else if (now - next > 100000000ull)
next = now; // fell far behind (debugger, sleep): resynchronise
}
CloseHandle(timer);
return 0;
+4
View File
@@ -180,5 +180,9 @@ void audio_mixer_update_effects(AudioRenderer *r);
void audio_mixer_effect_result(AudioRenderer *r, int id, void *output, unsigned size);
uint32_t audio_mixer_sink_written(AudioRenderer *r, int id);
void audio_mixer_render(AudioRenderer *r, double seconds); // after audio_voices_prepare, before voices advance
// Device pacing for the render thread at kern_now_ns `now`: 0 renders the next frame now, a positive value waits that
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
// device stalled).
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
+253
View File
@@ -0,0 +1,253 @@
// Generated by tools/build_shaders.py from runtime/shaders/bc3_encode.comp; do not edit.
// source sha256 ac318d358136b39a113a18474a815f447223667657c90f9e0356a392639d8541
#pragma once
#include <stdint.h>
static const uint32_t bc3_encode_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x00000314, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000007f, 0x00060010, 0x00000004,
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00030047, 0x0000001b, 0x00000002, 0x00050048,
0x0000001b, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x0000001b, 0x00000001, 0x00000023,
0x00000004, 0x00050048, 0x0000001b, 0x00000002, 0x00000023, 0x00000008, 0x00050048, 0x0000001b,
0x00000003, 0x00000023, 0x0000000c, 0x00050048, 0x0000001b, 0x00000004, 0x00000023, 0x00000010,
0x00050048, 0x0000001b, 0x00000005, 0x00000023, 0x00000014, 0x00040047, 0x0000002c, 0x00000006,
0x00000004, 0x00030047, 0x0000002d, 0x00000002, 0x00040048, 0x0000002d, 0x00000000, 0x00000018,
0x00050048, 0x0000002d, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x0000002f, 0x00000018,
0x00040047, 0x0000002f, 0x00000021, 0x00000000, 0x00040047, 0x0000002f, 0x00000022, 0x00000000,
0x00040047, 0x0000007f, 0x0000000b, 0x0000001c, 0x00040047, 0x000001e9, 0x00000006, 0x00000010,
0x00030047, 0x000001ea, 0x00000002, 0x00040048, 0x000001ea, 0x00000000, 0x00000019, 0x00050048,
0x000001ea, 0x00000000, 0x00000023, 0x00000000, 0x00030047, 0x000001ec, 0x00000019, 0x00040047,
0x000001ec, 0x00000021, 0x00000001, 0x00040047, 0x000001ec, 0x00000022, 0x00000000, 0x00040047,
0x0000020c, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
0x00040015, 0x00000006, 0x00000020, 0x00000000, 0x00030016, 0x00000008, 0x00000020, 0x00040017,
0x00000009, 0x00000008, 0x00000004, 0x00040017, 0x00000010, 0x00000008, 0x00000003, 0x0008001e,
0x0000001b, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00000006, 0x00040020,
0x0000001c, 0x00000009, 0x0000001b, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000009, 0x00040015,
0x0000001e, 0x00000020, 0x00000001, 0x0004002b, 0x0000001e, 0x0000001f, 0x00000002, 0x00040020,
0x00000020, 0x00000009, 0x00000006, 0x0004002b, 0x00000006, 0x00000023, 0x00000001, 0x0004002b,
0x0000001e, 0x00000027, 0x00000003, 0x0003001d, 0x0000002c, 0x00000006, 0x0003001e, 0x0000002d,
0x0000002c, 0x00040020, 0x0000002e, 0x0000000c, 0x0000002d, 0x0004003b, 0x0000002e, 0x0000002f,
0x0000000c, 0x0004002b, 0x0000001e, 0x00000030, 0x00000000, 0x00040020, 0x0000003f, 0x0000000c,
0x00000006, 0x0004002b, 0x00000008, 0x00000043, 0x437f0000, 0x00040017, 0x00000047, 0x00000006,
0x00000003, 0x0004002b, 0x00000008, 0x0000004b, 0x00000000, 0x0004002b, 0x00000006, 0x00000057,
0x00000000, 0x0004002b, 0x0000001e, 0x0000005a, 0x0000000b, 0x0004002b, 0x0000001e, 0x0000005e,
0x00000005, 0x0004002b, 0x00000006, 0x00000061, 0x00000002, 0x0004002b, 0x00000006, 0x00000069,
0x0000001f, 0x0004002b, 0x00000006, 0x00000070, 0x0000003f, 0x00040020, 0x0000007e, 0x00000001,
0x00000047, 0x0004003b, 0x0000007e, 0x0000007f, 0x00000001, 0x0004002b, 0x00000006, 0x00000084,
0x00000003, 0x0004002b, 0x00000006, 0x00000086, 0x00000004, 0x00020014, 0x00000088, 0x0004002b,
0x0000001e, 0x0000008b, 0x00000004, 0x00040020, 0x0000009a, 0x00000007, 0x00000009, 0x0007002c,
0x00000009, 0x0000009c, 0x00000043, 0x00000043, 0x00000043, 0x00000043, 0x0007002c, 0x00000009,
0x0000009e, 0x0000004b, 0x0000004b, 0x0000004b, 0x0000004b, 0x0004002b, 0x00000006, 0x000000a7,
0x00000010, 0x0004001c, 0x000000a9, 0x00000009, 0x000000a7, 0x00040020, 0x000000aa, 0x00000007,
0x000000a9, 0x0004002b, 0x0000001e, 0x000000d0, 0x00000001, 0x00040020, 0x000000d7, 0x00000007,
0x00000008, 0x00040017, 0x000000df, 0x00000006, 0x00000002, 0x0004002b, 0x0000001e, 0x000000e4,
0x00000008, 0x0004002b, 0x00000008, 0x00000101, 0x40e00000, 0x0004002b, 0x00000006, 0x00000110,
0x00000007, 0x0004002b, 0x00000006, 0x0000011f, 0x00000020, 0x0004002b, 0x00000008, 0x000001b5,
0x358637bd, 0x0004002b, 0x00000008, 0x000001ca, 0x40400000, 0x00040017, 0x000001e8, 0x00000006,
0x00000004, 0x0003001d, 0x000001e9, 0x000001e8, 0x0003001e, 0x000001ea, 0x000001e9, 0x00040020,
0x000001eb, 0x0000000c, 0x000001ea, 0x0004003b, 0x000001eb, 0x000001ec, 0x0000000c, 0x0004002b,
0x0000001e, 0x00000204, 0x00000010, 0x00040020, 0x00000209, 0x0000000c, 0x000001e8, 0x0004002b,
0x00000006, 0x0000020b, 0x00000008, 0x0006002c, 0x00000047, 0x0000020c, 0x0000020b, 0x0000020b,
0x00000023, 0x0006002c, 0x00000010, 0x00000302, 0x0000004b, 0x0000004b, 0x0000004b, 0x0006002c,
0x00000010, 0x00000303, 0x00000043, 0x00000043, 0x00000043, 0x0004002b, 0x00000008, 0x00000304,
0x3d800000, 0x0007002c, 0x00000009, 0x00000305, 0x00000304, 0x00000304, 0x00000304, 0x00000304,
0x0006002c, 0x00000010, 0x00000306, 0x00000304, 0x00000304, 0x00000304, 0x0004002b, 0x00000008,
0x00000309, 0x3df8f8fa, 0x0004002b, 0x00000008, 0x0000030a, 0x3e7cfcfe, 0x0006002c, 0x00000010,
0x0000030b, 0x00000309, 0x0000030a, 0x00000309, 0x0004002b, 0x00000008, 0x0000030d, 0x41039ce7,
0x0004002b, 0x00000008, 0x0000030f, 0x40818619, 0x0004002b, 0x00000006, 0x00000310, 0x00000013,
0x0004002b, 0x00000006, 0x00000311, 0xfffffff0, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
0x00000003, 0x000200f8, 0x00000005, 0x0004003b, 0x000000aa, 0x000000ab, 0x00000007, 0x000300f7,
0x0000020d, 0x00000000, 0x000300fb, 0x00000057, 0x0000020e, 0x000200f8, 0x0000020e, 0x0004003d,
0x00000047, 0x00000080, 0x0000007f, 0x00050041, 0x00000020, 0x00000082, 0x0000001d, 0x00000027,
0x0004003d, 0x00000006, 0x00000083, 0x00000082, 0x00050080, 0x00000006, 0x00000085, 0x00000083,
0x00000084, 0x00050086, 0x00000006, 0x00000087, 0x00000085, 0x00000086, 0x00050051, 0x00000006,
0x0000008a, 0x00000080, 0x00000000, 0x00050041, 0x00000020, 0x0000008c, 0x0000001d, 0x0000008b,
0x0004003d, 0x00000006, 0x0000008d, 0x0000008c, 0x000500ae, 0x00000088, 0x0000008e, 0x0000008a,
0x0000008d, 0x000400a8, 0x00000088, 0x0000008f, 0x0000008e, 0x000300f7, 0x00000091, 0x00000000,
0x000400fa, 0x0000008f, 0x00000090, 0x00000091, 0x000200f8, 0x00000090, 0x00050051, 0x00000006,
0x00000093, 0x00000080, 0x00000001, 0x000500ae, 0x00000088, 0x00000095, 0x00000093, 0x00000087,
0x000200f9, 0x00000091, 0x000200f8, 0x00000091, 0x000700f5, 0x00000088, 0x00000096, 0x0000008e,
0x0000020e, 0x00000095, 0x00000090, 0x000300f7, 0x00000098, 0x00000000, 0x000400fa, 0x00000096,
0x00000097, 0x00000098, 0x000200f8, 0x00000097, 0x000200f9, 0x0000020d, 0x000200f8, 0x00000098,
0x000200f9, 0x000000a1, 0x000200f8, 0x000000a1, 0x000700f5, 0x00000009, 0x000002b0, 0x0000009c,
0x00000098, 0x000000c4, 0x000000a2, 0x000700f5, 0x00000009, 0x000002af, 0x0000009e, 0x00000098,
0x000000c9, 0x000000a2, 0x000700f5, 0x00000009, 0x000002ae, 0x0000009e, 0x00000098, 0x000000ce,
0x000000a2, 0x000700f5, 0x00000006, 0x000002ad, 0x00000057, 0x00000098, 0x000000d1, 0x000000a2,
0x000500b0, 0x00000088, 0x000000a8, 0x000002ad, 0x000000a7, 0x000400f6, 0x000000a3, 0x000000a2,
0x00000000, 0x000400fa, 0x000000a8, 0x000000a2, 0x000000a3, 0x000200f8, 0x000000a2, 0x00050084,
0x00000006, 0x000000af, 0x0000008a, 0x00000086, 0x000500c7, 0x00000006, 0x000000b1, 0x000002ad,
0x00000084, 0x00050080, 0x00000006, 0x000000b2, 0x000000af, 0x000000b1, 0x00050051, 0x00000006,
0x000000b4, 0x00000080, 0x00000001, 0x00050084, 0x00000006, 0x000000b5, 0x000000b4, 0x00000086,
0x000500c2, 0x00000006, 0x000000b7, 0x000002ad, 0x0000001f, 0x00050080, 0x00000006, 0x000000b8,
0x000000b5, 0x000000b7, 0x00050051, 0x00000006, 0x000000bd, 0x00000080, 0x00000002, 0x00050041,
0x00000020, 0x00000216, 0x0000001d, 0x0000001f, 0x0004003d, 0x00000006, 0x00000217, 0x00000216,
0x00050082, 0x00000006, 0x00000218, 0x00000217, 0x00000023, 0x0007000c, 0x00000006, 0x00000219,
0x00000001, 0x00000026, 0x000000b2, 0x00000218, 0x00050082, 0x00000006, 0x0000021d, 0x00000083,
0x00000023, 0x0007000c, 0x00000006, 0x0000021e, 0x00000001, 0x00000026, 0x000000b8, 0x0000021d,
0x00050041, 0x00000020, 0x0000021f, 0x0000001d, 0x00000030, 0x0004003d, 0x00000006, 0x00000220,
0x0000021f, 0x00050084, 0x00000006, 0x00000224, 0x000000bd, 0x00000083, 0x00050080, 0x00000006,
0x00000226, 0x00000224, 0x0000021e, 0x00050084, 0x00000006, 0x00000229, 0x00000226, 0x00000217,
0x00050080, 0x00000006, 0x0000022a, 0x00000220, 0x00000229, 0x00050080, 0x00000006, 0x0000022c,
0x0000022a, 0x00000219, 0x00060041, 0x0000003f, 0x0000022d, 0x0000002f, 0x00000030, 0x0000022c,
0x0004003d, 0x00000006, 0x0000022e, 0x0000022d, 0x0006000c, 0x00000009, 0x0000022f, 0x00000001,
0x00000040, 0x0000022e, 0x0005008e, 0x00000009, 0x00000230, 0x0000022f, 0x00000043, 0x00050041,
0x0000009a, 0x000000bf, 0x000000ab, 0x000002ad, 0x0003003e, 0x000000bf, 0x00000230, 0x0004003d,
0x00000009, 0x000000c3, 0x000000bf, 0x0007000c, 0x00000009, 0x000000c4, 0x00000001, 0x00000025,
0x000002b0, 0x000000c3, 0x0004003d, 0x00000009, 0x000000c8, 0x000000bf, 0x0007000c, 0x00000009,
0x000000c9, 0x00000001, 0x00000028, 0x000002af, 0x000000c8, 0x0004003d, 0x00000009, 0x000000cc,
0x000000bf, 0x00050081, 0x00000009, 0x000000ce, 0x000002ae, 0x000000cc, 0x00050080, 0x00000006,
0x000000d1, 0x000002ad, 0x000000d0, 0x000200f9, 0x000000a1, 0x000200f8, 0x000000a3, 0x00050085,
0x00000009, 0x000000d5, 0x000002ae, 0x00000305, 0x00050051, 0x00000008, 0x000000d9, 0x000002af,
0x00000003, 0x0004006d, 0x00000006, 0x000000da, 0x000000d9, 0x00050051, 0x00000008, 0x000000dd,
0x000002b0, 0x00000003, 0x0004006d, 0x00000006, 0x000000de, 0x000000dd, 0x000500c4, 0x00000006,
0x000000e5, 0x000000de, 0x000000e4, 0x000500c5, 0x00000006, 0x000000e6, 0x000000da, 0x000000e5,
0x00050050, 0x000000df, 0x000000e7, 0x000000e6, 0x00000057, 0x000500ac, 0x00000088, 0x000000ea,
0x000000da, 0x000000de, 0x000300f7, 0x000000ec, 0x00000000, 0x000400fa, 0x000000ea, 0x000000eb,
0x000000ec, 0x000200f8, 0x000000eb, 0x00050082, 0x00000006, 0x000000f0, 0x000000da, 0x000000de,
0x00040070, 0x00000008, 0x000000f1, 0x000000f0, 0x000200f9, 0x000000f3, 0x000200f8, 0x000000f3,
0x000700f5, 0x000000df, 0x000002c8, 0x000000e7, 0x000000eb, 0x000002f1, 0x000000f6, 0x000700f5,
0x00000006, 0x000002b1, 0x00000057, 0x000000eb, 0x00000143, 0x000000f6, 0x000500b0, 0x00000088,
0x000000f9, 0x000002b1, 0x000000a7, 0x000400f6, 0x000000f5, 0x000000f6, 0x00000000, 0x000400fa,
0x000000f9, 0x000000f4, 0x000000f5, 0x000200f8, 0x000000f4, 0x00040070, 0x00000008, 0x000000fc,
0x000000da, 0x00060041, 0x000000d7, 0x000000fe, 0x000000ab, 0x000002b1, 0x00000084, 0x0004003d,
0x00000008, 0x000000ff, 0x000000fe, 0x00050083, 0x00000008, 0x00000100, 0x000000fc, 0x000000ff,
0x00050085, 0x00000008, 0x00000102, 0x00000100, 0x00000101, 0x00050088, 0x00000008, 0x00000104,
0x00000102, 0x000000f1, 0x0006000c, 0x00000008, 0x00000105, 0x00000001, 0x00000001, 0x00000104,
0x0008000c, 0x00000008, 0x00000106, 0x00000001, 0x0000002b, 0x00000105, 0x0000004b, 0x00000101,
0x0004006d, 0x00000006, 0x00000107, 0x00000106, 0x000500aa, 0x00000088, 0x0000010a, 0x00000107,
0x00000057, 0x000300f7, 0x0000010d, 0x00000000, 0x000400fa, 0x0000010a, 0x0000010c, 0x0000010e,
0x000200f8, 0x0000010c, 0x000200f9, 0x0000010d, 0x000200f8, 0x0000010e, 0x000500aa, 0x00000088,
0x00000111, 0x00000107, 0x00000110, 0x000300f7, 0x00000114, 0x00000000, 0x000400fa, 0x00000111,
0x00000113, 0x00000115, 0x000200f8, 0x00000113, 0x000200f9, 0x00000114, 0x000200f8, 0x00000115,
0x00050080, 0x00000006, 0x00000117, 0x00000107, 0x00000023, 0x000200f9, 0x00000114, 0x000200f8,
0x00000114, 0x000700f5, 0x00000006, 0x000002db, 0x00000023, 0x00000113, 0x00000117, 0x00000115,
0x000200f9, 0x0000010d, 0x000200f8, 0x0000010d, 0x000700f5, 0x00000006, 0x000002dc, 0x00000057,
0x0000010c, 0x000002db, 0x00000114, 0x00050084, 0x00000006, 0x0000011c, 0x00000084, 0x000002b1,
0x00050080, 0x00000006, 0x0000011d, 0x000000a7, 0x0000011c, 0x000500b0, 0x00000088, 0x00000120,
0x0000011d, 0x0000011f, 0x000300f7, 0x00000122, 0x00000000, 0x000400fa, 0x00000120, 0x00000121,
0x00000122, 0x000200f8, 0x00000121, 0x000500c4, 0x00000006, 0x00000125, 0x000002dc, 0x0000011d,
0x00050051, 0x00000006, 0x00000127, 0x000002c8, 0x00000000, 0x000500c5, 0x00000006, 0x00000128,
0x00000127, 0x00000125, 0x00060052, 0x000000df, 0x0000028e, 0x00000128, 0x000002c8, 0x00000000,
0x000200f9, 0x00000122, 0x000200f8, 0x00000122, 0x000700f5, 0x000000df, 0x000002e3, 0x000002c8,
0x0000010d, 0x0000028e, 0x00000121, 0x00050080, 0x00000006, 0x0000012b, 0x0000011c, 0x00000310,
0x000500ac, 0x00000088, 0x0000012c, 0x0000012b, 0x0000011f, 0x000300f7, 0x0000012e, 0x00000000,
0x000400fa, 0x0000012c, 0x0000012d, 0x0000012e, 0x000200f8, 0x0000012d, 0x0007000c, 0x00000006,
0x00000131, 0x00000001, 0x00000026, 0x0000011d, 0x0000011f, 0x00050082, 0x00000006, 0x00000132,
0x0000011f, 0x00000131, 0x000500c2, 0x00000006, 0x00000133, 0x000002dc, 0x00000132, 0x000500ac,
0x00000088, 0x00000135, 0x0000011d, 0x0000011f, 0x000300f7, 0x00000138, 0x00000000, 0x000400fa,
0x00000135, 0x00000137, 0x0000013b, 0x000200f8, 0x00000137, 0x00050080, 0x00000006, 0x0000013a,
0x0000011c, 0x00000311, 0x000200f9, 0x00000138, 0x000200f8, 0x0000013b, 0x000200f9, 0x00000138,
0x000200f8, 0x00000138, 0x000700f5, 0x00000006, 0x000002e1, 0x0000013a, 0x00000137, 0x00000057,
0x0000013b, 0x000500c4, 0x00000006, 0x0000013d, 0x00000133, 0x000002e1, 0x00050051, 0x00000006,
0x0000013f, 0x000002e3, 0x00000001, 0x000500c5, 0x00000006, 0x00000140, 0x0000013f, 0x0000013d,
0x00060052, 0x000000df, 0x00000291, 0x00000140, 0x000002e3, 0x00000001, 0x000200f9, 0x0000012e,
0x000200f8, 0x0000012e, 0x000700f5, 0x000000df, 0x000002f1, 0x000002e3, 0x00000122, 0x00000291,
0x00000138, 0x000200f9, 0x000000f6, 0x000200f8, 0x000000f6, 0x00050080, 0x00000006, 0x00000143,
0x000002b1, 0x000000d0, 0x000200f9, 0x000000f3, 0x000200f8, 0x000000f5, 0x000200f9, 0x000000ec,
0x000200f8, 0x000000ec, 0x000700f5, 0x000000df, 0x000002c7, 0x000000e7, 0x000000a3, 0x000002c8,
0x000000f5, 0x0008004f, 0x00000010, 0x00000146, 0x000002b0, 0x000002b0, 0x00000000, 0x00000001,
0x00000002, 0x0008004f, 0x00000010, 0x00000149, 0x000002af, 0x000002af, 0x00000000, 0x00000001,
0x00000002, 0x000200f9, 0x0000014d, 0x000200f8, 0x0000014d, 0x000700f5, 0x00000008, 0x000002bb,
0x0000004b, 0x000000ec, 0x00000169, 0x0000014e, 0x000700f5, 0x00000008, 0x000002b7, 0x0000004b,
0x000000ec, 0x00000162, 0x0000014e, 0x000700f5, 0x00000006, 0x000002b6, 0x00000057, 0x000000ec,
0x0000016b, 0x0000014e, 0x000500b0, 0x00000088, 0x00000153, 0x000002b6, 0x000000a7, 0x000400f6,
0x0000014f, 0x0000014e, 0x00000000, 0x000400fa, 0x00000153, 0x0000014e, 0x0000014f, 0x000200f8,
0x0000014e, 0x00050041, 0x0000009a, 0x00000156, 0x000000ab, 0x000002b6, 0x0004003d, 0x00000009,
0x00000157, 0x00000156, 0x0008004f, 0x00000010, 0x00000158, 0x00000157, 0x00000157, 0x00000000,
0x00000001, 0x00000002, 0x0008004f, 0x00000010, 0x0000015a, 0x000000d5, 0x000000d5, 0x00000000,
0x00000001, 0x00000002, 0x00050083, 0x00000010, 0x0000015b, 0x00000158, 0x0000015a, 0x00050051,
0x00000008, 0x0000015d, 0x0000015b, 0x00000000, 0x00050051, 0x00000008, 0x0000015f, 0x0000015b,
0x00000001, 0x00050085, 0x00000008, 0x00000160, 0x0000015d, 0x0000015f, 0x00050081, 0x00000008,
0x00000162, 0x000002b7, 0x00000160, 0x00050051, 0x00000008, 0x00000164, 0x0000015b, 0x00000002,
0x00050085, 0x00000008, 0x00000167, 0x00000164, 0x0000015f, 0x00050081, 0x00000008, 0x00000169,
0x000002bb, 0x00000167, 0x00050080, 0x00000006, 0x0000016b, 0x000002b6, 0x000000d0, 0x000200f9,
0x0000014d, 0x000200f8, 0x0000014f, 0x000500b8, 0x00000088, 0x0000016d, 0x000002b7, 0x0000004b,
0x000300f7, 0x0000016f, 0x00000000, 0x000400fa, 0x0000016d, 0x0000016e, 0x0000016f, 0x000200f8,
0x0000016e, 0x00050051, 0x00000008, 0x00000172, 0x000002b0, 0x00000000, 0x00050051, 0x00000008,
0x00000174, 0x000002af, 0x00000000, 0x00060052, 0x00000010, 0x00000299, 0x00000174, 0x00000146,
0x00000000, 0x00060052, 0x00000010, 0x0000029b, 0x00000172, 0x00000149, 0x00000000, 0x000200f9,
0x0000016f, 0x000200f8, 0x0000016f, 0x000700f5, 0x00000010, 0x000002bd, 0x00000149, 0x0000014f,
0x0000029b, 0x0000016e, 0x000700f5, 0x00000010, 0x000002bc, 0x00000146, 0x0000014f, 0x00000299,
0x0000016e, 0x000500b8, 0x00000088, 0x00000179, 0x000002bb, 0x0000004b, 0x000300f7, 0x0000017b,
0x00000000, 0x000400fa, 0x00000179, 0x0000017a, 0x0000017b, 0x000200f8, 0x0000017a, 0x00050051,
0x00000008, 0x0000017e, 0x000002bc, 0x00000002, 0x00050051, 0x00000008, 0x00000180, 0x000002bd,
0x00000002, 0x00060052, 0x00000010, 0x0000029f, 0x00000180, 0x000002bc, 0x00000002, 0x00060052,
0x00000010, 0x000002a1, 0x0000017e, 0x000002bd, 0x00000002, 0x000200f9, 0x0000017b, 0x000200f8,
0x0000017b, 0x000700f5, 0x00000010, 0x000002bf, 0x000002bc, 0x0000016f, 0x0000029f, 0x0000017a,
0x000700f5, 0x00000010, 0x000002be, 0x000002bd, 0x0000016f, 0x000002a1, 0x0000017a, 0x00050083,
0x00000010, 0x00000187, 0x000002be, 0x000002bf, 0x00050085, 0x00000010, 0x00000189, 0x00000187,
0x00000306, 0x00050083, 0x00000010, 0x0000018d, 0x000002be, 0x00000189, 0x0008000c, 0x00000010,
0x00000237, 0x00000001, 0x0000002b, 0x0000018d, 0x00000302, 0x00000303, 0x00050085, 0x00000010,
0x0000023a, 0x00000237, 0x0000030b, 0x0006000c, 0x00000010, 0x0000023b, 0x00000001, 0x00000001,
0x0000023a, 0x0004006d, 0x00000047, 0x0000023c, 0x0000023b, 0x00050051, 0x00000006, 0x0000023e,
0x0000023c, 0x00000000, 0x000500c4, 0x00000006, 0x0000023f, 0x0000023e, 0x0000005a, 0x00050051,
0x00000006, 0x00000241, 0x0000023c, 0x00000001, 0x000500c4, 0x00000006, 0x00000242, 0x00000241,
0x0000005e, 0x000500c5, 0x00000006, 0x00000243, 0x0000023f, 0x00000242, 0x00050051, 0x00000006,
0x00000245, 0x0000023c, 0x00000002, 0x000500c5, 0x00000006, 0x00000246, 0x00000243, 0x00000245,
0x00050081, 0x00000010, 0x00000193, 0x000002bf, 0x00000189, 0x0008000c, 0x00000010, 0x0000024d,
0x00000001, 0x0000002b, 0x00000193, 0x00000302, 0x00000303, 0x00050085, 0x00000010, 0x00000250,
0x0000024d, 0x0000030b, 0x0006000c, 0x00000010, 0x00000251, 0x00000001, 0x00000001, 0x00000250,
0x0004006d, 0x00000047, 0x00000252, 0x00000251, 0x00050051, 0x00000006, 0x00000254, 0x00000252,
0x00000000, 0x000500c4, 0x00000006, 0x00000255, 0x00000254, 0x0000005a, 0x00050051, 0x00000006,
0x00000257, 0x00000252, 0x00000001, 0x000500c4, 0x00000006, 0x00000258, 0x00000257, 0x0000005e,
0x000500c5, 0x00000006, 0x00000259, 0x00000255, 0x00000258, 0x00050051, 0x00000006, 0x0000025b,
0x00000252, 0x00000002, 0x000500c5, 0x00000006, 0x0000025c, 0x00000259, 0x0000025b, 0x000500ab,
0x00000088, 0x00000199, 0x00000246, 0x0000025c, 0x000300f7, 0x0000019b, 0x00000000, 0x000400fa,
0x00000199, 0x0000019a, 0x0000019b, 0x000200f8, 0x0000019a, 0x000500b0, 0x00000088, 0x0000019e,
0x00000246, 0x0000025c, 0x000600a9, 0x00000006, 0x00000312, 0x0000019e, 0x00000246, 0x0000025c,
0x000600a9, 0x00000006, 0x00000313, 0x0000019e, 0x0000025c, 0x00000246, 0x000500c2, 0x00000006,
0x00000260, 0x00000313, 0x0000005a, 0x000500c7, 0x00000006, 0x00000261, 0x00000260, 0x00000069,
0x00040070, 0x00000008, 0x00000262, 0x00000261, 0x00050085, 0x00000008, 0x00000264, 0x00000262,
0x0000030d, 0x000500c2, 0x00000006, 0x00000266, 0x00000313, 0x0000005e, 0x000500c7, 0x00000006,
0x00000267, 0x00000266, 0x00000070, 0x00040070, 0x00000008, 0x00000268, 0x00000267, 0x00050085,
0x00000008, 0x0000026a, 0x00000268, 0x0000030f, 0x000500c7, 0x00000006, 0x0000026c, 0x00000313,
0x00000069, 0x00040070, 0x00000008, 0x0000026d, 0x0000026c, 0x00050085, 0x00000008, 0x0000026f,
0x0000026d, 0x0000030d, 0x00060050, 0x00000010, 0x00000270, 0x00000264, 0x0000026a, 0x0000026f,
0x000500c2, 0x00000006, 0x00000274, 0x00000312, 0x0000005a, 0x000500c7, 0x00000006, 0x00000275,
0x00000274, 0x00000069, 0x00040070, 0x00000008, 0x00000276, 0x00000275, 0x00050085, 0x00000008,
0x00000278, 0x00000276, 0x0000030d, 0x000500c2, 0x00000006, 0x0000027a, 0x00000312, 0x0000005e,
0x000500c7, 0x00000006, 0x0000027b, 0x0000027a, 0x00000070, 0x00040070, 0x00000008, 0x0000027c,
0x0000027b, 0x00050085, 0x00000008, 0x0000027e, 0x0000027c, 0x0000030f, 0x000500c7, 0x00000006,
0x00000280, 0x00000312, 0x00000069, 0x00040070, 0x00000008, 0x00000281, 0x00000280, 0x00050085,
0x00000008, 0x00000283, 0x00000281, 0x0000030d, 0x00060050, 0x00000010, 0x00000284, 0x00000278,
0x0000027e, 0x00000283, 0x00050083, 0x00000010, 0x000001b0, 0x00000284, 0x00000270, 0x00050094,
0x00000008, 0x000001b4, 0x000001b0, 0x000001b0, 0x0007000c, 0x00000008, 0x000001b6, 0x00000001,
0x00000028, 0x000001b4, 0x000001b5, 0x000200f9, 0x000001b8, 0x000200f8, 0x000001b8, 0x000700f5,
0x00000006, 0x000002d0, 0x00000057, 0x0000019a, 0x000001e5, 0x000001bb, 0x000700f5, 0x00000006,
0x000002c2, 0x00000057, 0x0000019a, 0x000001e7, 0x000001bb, 0x000500b0, 0x00000088, 0x000001be,
0x000002c2, 0x000000a7, 0x000400f6, 0x000001ba, 0x000001bb, 0x00000000, 0x000400fa, 0x000001be,
0x000001b9, 0x000001ba, 0x000200f8, 0x000001b9, 0x00050041, 0x0000009a, 0x000001c1, 0x000000ab,
0x000002c2, 0x0004003d, 0x00000009, 0x000001c2, 0x000001c1, 0x0008004f, 0x00000010, 0x000001c3,
0x000001c2, 0x000001c2, 0x00000000, 0x00000001, 0x00000002, 0x00050083, 0x00000010, 0x000001c5,
0x000001c3, 0x00000270, 0x00050094, 0x00000008, 0x000001c7, 0x000001c5, 0x000001b0, 0x00050088,
0x00000008, 0x000001c9, 0x000001c7, 0x000001b6, 0x00050085, 0x00000008, 0x000001cb, 0x000001c9,
0x000001ca, 0x0006000c, 0x00000008, 0x000001cc, 0x00000001, 0x00000001, 0x000001cb, 0x0008000c,
0x00000008, 0x000001cd, 0x00000001, 0x0000002b, 0x000001cc, 0x0000004b, 0x000001ca, 0x0004006d,
0x00000006, 0x000001ce, 0x000001cd, 0x000500aa, 0x00000088, 0x000001d1, 0x000001ce, 0x00000057,
0x000300f7, 0x000001d4, 0x00000000, 0x000400fa, 0x000001d1, 0x000001d3, 0x000001d5, 0x000200f8,
0x000001d3, 0x000200f9, 0x000001d4, 0x000200f8, 0x000001d5, 0x000500aa, 0x00000088, 0x000001d7,
0x000001ce, 0x00000084, 0x000300f7, 0x000001da, 0x00000000, 0x000400fa, 0x000001d7, 0x000001d9,
0x000001db, 0x000200f8, 0x000001d9, 0x000200f9, 0x000001da, 0x000200f8, 0x000001db, 0x00050080,
0x00000006, 0x000001dd, 0x000001ce, 0x00000023, 0x000200f9, 0x000001da, 0x000200f8, 0x000001da,
0x000700f5, 0x00000006, 0x000002d2, 0x00000023, 0x000001d9, 0x000001dd, 0x000001db, 0x000200f9,
0x000001d4, 0x000200f8, 0x000001d4, 0x000700f5, 0x00000006, 0x000002d3, 0x00000057, 0x000001d3,
0x000002d2, 0x000001da, 0x00050084, 0x00000006, 0x000001e2, 0x00000061, 0x000002c2, 0x000500c4,
0x00000006, 0x000001e3, 0x000002d3, 0x000001e2, 0x000500c5, 0x00000006, 0x000001e5, 0x000002d0,
0x000001e3, 0x000200f9, 0x000001bb, 0x000200f8, 0x000001bb, 0x00050080, 0x00000006, 0x000001e7,
0x000002c2, 0x000000d0, 0x000200f9, 0x000001b8, 0x000200f8, 0x000001ba, 0x000200f9, 0x0000019b,
0x000200f8, 0x0000019b, 0x000700f5, 0x00000006, 0x000002cf, 0x00000057, 0x0000017b, 0x000002d0,
0x000001ba, 0x000700f5, 0x00000006, 0x000002cd, 0x0000025c, 0x0000017b, 0x00000312, 0x000001ba,
0x000700f5, 0x00000006, 0x000002cb, 0x00000246, 0x0000017b, 0x00000313, 0x000001ba, 0x00050041,
0x00000020, 0x000001ed, 0x0000001d, 0x000000d0, 0x0004003d, 0x00000006, 0x000001ee, 0x000001ed,
0x00050051, 0x00000006, 0x000001f0, 0x00000080, 0x00000002, 0x00050041, 0x00000020, 0x000001f1,
0x0000001d, 0x0000005e, 0x0004003d, 0x00000006, 0x000001f2, 0x000001f1, 0x00050084, 0x00000006,
0x000001f3, 0x000001f0, 0x000001f2, 0x00050080, 0x00000006, 0x000001f4, 0x000001ee, 0x000001f3,
0x00050051, 0x00000006, 0x000001f6, 0x00000080, 0x00000001, 0x00050084, 0x00000006, 0x000001f9,
0x000001f6, 0x0000008d, 0x00050080, 0x00000006, 0x000001fa, 0x000001f4, 0x000001f9, 0x00050080,
0x00000006, 0x000001fd, 0x000001fa, 0x0000008a, 0x00050051, 0x00000006, 0x000001ff, 0x000002c7,
0x00000000, 0x00050051, 0x00000006, 0x00000201, 0x000002c7, 0x00000001, 0x000500c4, 0x00000006,
0x00000205, 0x000002cd, 0x00000204, 0x000500c5, 0x00000006, 0x00000206, 0x000002cb, 0x00000205,
0x00070050, 0x000001e8, 0x00000208, 0x000001ff, 0x00000201, 0x00000206, 0x000002cf, 0x00060041,
0x00000209, 0x0000020a, 0x000001ec, 0x00000030, 0x000001fd, 0x0003003e, 0x0000020a, 0x00000208,
0x000200f9, 0x0000020d, 0x000200f8, 0x0000020d, 0x000100fd, 0x00010038,
};
+12 -11
View File
@@ -30,12 +30,12 @@ static int save_ok = 1; // the last cfg_save_to wrote (o
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
static const char *const known[] = {
"AB", "APP_POOL", "ASSUME_FOCUS", "ASTC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_OFF", "AUDIO_PREFILL_MS",
"AB", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_OFF", "AUDIO_PREFILL_MS",
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
"BIND_ZL", "BIND_ZR", "CLOTH_REALTIME", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
"EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "FLIGHT_SPIKE_MS",
"FLIGHT_THUMBS", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "GPL", "GPL_DRAW_BUILD", "GPU",
"GPU_INDEX", "GPU_NAME", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
@@ -43,12 +43,12 @@ static const char *const known[] = {
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PATCH_OUTPUT", "PATCH_SCENE", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
"PROFILE_MAIN", "PROFILE_RENDER", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REEXEC",
"PROFILE_MAIN", "PROFILE_RENDER", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
"RENDER_SCALE", "REPLAY_ASYNC",
"REPLAY_CHECK", "RESOLVE_CHECK", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
"SHADER_MEMO", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR", "SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"SHADER_MEMO", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR", "SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
"WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
};
@@ -59,11 +59,11 @@ static const struct { const char *key, *section; } sections[] = {
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
{"SHARPNESS", "display"},
{"SHARPNESS", "display"}, {"ASPECT", "display"},
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
{"VOLUME", "audio"},
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
{"PAD_LAYOUT", "input"}, {"MOUSE_SENS", "input"}, {"INPUT_RECORD", "input"},
{"PAD_LAYOUT", "input"}, {"STICK_DEADZONE", "input"}, {"MOUSE_SENS", "input"}, {"INPUT_RECORD", "input"},
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
@@ -74,7 +74,7 @@ static const struct { const char *key, *section; } sections[] = {
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
{"VK_DYN_VERTEX", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
@@ -94,6 +94,7 @@ static const CfgMeta metas[] = {
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
{"ASPECT", "fit", "fit|stretch", LIVE},
// Game
{"FPS_LIMIT", "60", "30|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
@@ -103,8 +104,8 @@ static const CfgMeta metas[] = {
// Sound, Controls, Menu
{"VOLUME", "100", "0..100", LIVE},
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox", LIVE},
{"MOUSE_SENS", "1", "0..10", LIVE},
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox|xbox_prompts", LIVE},
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"MOUSE_SENS", "1", "0..10", LIVE},
{"MENU_LANG", "", "en|ru", LIVE},
// Advanced: scheduling
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
@@ -115,7 +116,7 @@ static const CfgMeta metas[] = {
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "", "0|1", RESTART},
// textures and memory
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "0", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
// render fixes
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
+21
View File
@@ -261,11 +261,25 @@ int probe_cfg(void) {
CHECK((int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768, "mouse: clamped to the unit circle");
input_mouse_stick(30, 0, 1.0 / 60, 0, st);
CHECK(st[0] == 0 && st[1] == 0, "mouse: sensitivity 0 is off");
input_pad_stick(3000, -2000, 0.15f, st);
CHECK(st[0] == 0 && st[1] == 0, "pad stick: an 11% drift inside a 15% dead zone reads centred");
input_pad_stick(4000, -3000, 0, st);
CHECK(st[0] == 4000 && st[1] == -3000, "pad stick: dead zone 0 passes the stick unchanged");
input_pad_stick(32767, 0, 0.15f, st);
CHECK(st[0] == 32767 && st[1] == 0, "pad stick: full deflection still reaches full");
input_pad_stick(-32768, -32768, 0.15f, st);
CHECK(st[0] < 0 && st[1] < 0 && st[0] >= -32767 && (int64_t)st[0] * st[0] + (int64_t)st[1] * st[1] <= 32768ll * 32768,
"pad stick: a square gate's corner stays on the unit circle");
input_pad_stick(0, 32767 / 2 + 1, 0.2f, st);
CHECK(abs(st[1] - 32767 * 3 / 8) <= 3, "pad stick: half deflection past a 20% zone is (0.5 - 0.2) / 0.8 of full");
// Gamepad layouts (input_map.c)
CHECK(input_pad_layout_pick("auto", 1) == PAD_LAYOUT_NINTENDO && input_pad_layout_pick(NULL, 0) == PAD_LAYOUT_XBOX,
"pad layout: automatic is Nintendo for an east button labelled A, Xbox otherwise");
CHECK(input_pad_layout_pick("Xbox", 1) == PAD_LAYOUT_XBOX && input_pad_layout_pick("nintendo", 0) == PAD_LAYOUT_NINTENDO,
"pad layout: a set layout wins over the labels");
CHECK(input_pad_layout_pick("xbox_prompts", 0) == PAD_LAYOUT_NINTENDO &&
input_pad_layout_pick("Xbox_Prompts", 1) == PAD_LAYOUT_NINTENDO,
"pad layout: Xbox prompts keep the Nintendo positions (only the prompts change, romfs.c overlay_init)");
uint8_t pd[PAD_COUNT] = {0};
pd[PAD_SOUTH] = pd[PAD_NORTH] = 1;
CHECK(input_map_pad_buttons(pd, PAD_LAYOUT_NINTENDO) == (1ull << B_B | 1ull << B_X) &&
@@ -280,6 +294,13 @@ int probe_cfg(void) {
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_SOUTH] = 1}, PAD_LAYOUT_XBOX) == 0,
"pad layout: a bind line replaces the layout's own button");
input_map_reset();
CHECK(input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_XBOX) == 0,
"pad layout: a back paddle is unbound by default");
CHECK(input_map_bind("ZL", "pad:default, pad:rpaddle1") &&
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_RPADDLE1] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL &&
input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_ZL] = 1}, PAD_LAYOUT_NINTENDO) == 1ull << B_ZL,
"bind: pad:rpaddle1 adds a back paddle, pad:default keeps the trigger");
input_map_reset();
// Bind lines for stick directions and default parts; the remapping rows' edits (input_map.c)
memset(dn, 0, sizeof dn);
dn[KEY_I] = 1;
+6
View File
@@ -12,6 +12,7 @@
// is also fixed at init; they are looked up only after the flat table misses.
#include "dispatch.h"
#include "host.h"
#include "platform/platform.h"
#include <stdarg.h>
#include <stdio.h>
@@ -51,6 +52,11 @@ void rt_fatal(const char *fmt, ...) {
rt_vlogf(fmt, ap);
rt_log_raw("\n", 1);
va_end(ap);
// A failed allocation (malloc, realloc, VirtualAlloc) means the commit limit was reached: this line tells a leak of
// ours (process private bytes near the limit) from a machine short of page file.
double proc, used, limit;
plat_commit_info(&proc, &used, &limit);
rt_log("fatal: memory commit: this process %.0f MB, the system %.0f of %.0f MB", proc, used, limit);
fflush(stdout);
fflush(stderr);
_Exit(70);
+2
View File
@@ -38,6 +38,8 @@ uint64_t romfs_file_count(void); // files in the index
const char *romfs_entry(uint64_t i, uint64_t *offset, uint64_t *size); // path, image offset and size of index row i
const char *romfs_image_path(void); // the romfs.bin path fs_init opened
size_t romfs_overlay_count(void);
int romfs_xbox_prompts_ready(void); // romfs-patches-xbox was there at start
int romfs_xbox_prompts_active(void); // and was loaded (pad_layout = xbox_prompts at start)
const char *romfs_overlay(size_t k, const char **file, uint64_t *size, int *is_added); // RomFS path of overlay k
void fs_trace(const char *what, const char *path); // TOTK_TRACE=fs: one line per open
+136 -11
View File
@@ -18,6 +18,11 @@
// (64 hex digits) overrides it. A translator change therefore changes every cache key without a hand-made KEY_VERSION
// bump; KEY_VERSION is for changes on this side (key layout, how the output is used).
//
// Pack: tools/pack_shader_cache.py folds the .spv, .res.json, .use and in/*.in files into <cache>/shader-cache.pack
// (one file instead of four per shader: ~117,000 files after an install) and deletes them; the installer runs it after
// preparing the shaders. The pack is read-only here. Every read looks in the pack first and then for the loose file,
// and new translations are written as loose files as before, until the tool runs again (section "cache pack").
//
// Stored inputs: every translation also writes <cache>/in/<input id>.in (magic "TKSI"; stage, the full answers, the
// control section, the code up to its length, c1), so a pipeline key record (vk_pipeline.c), which names its shaders by
// input id, can be translated again under a new translator without guest memory (gfx_shader_load_input,
@@ -125,6 +130,94 @@ static INIT_ONCE paths_once = INIT_ONCE_STATIC_INIT;
#define read_file plat_file_read
#define write_file plat_file_write
// ---- cache pack (<cache>/shader-cache.pack, written by tools/pack_shader_cache.py)
// Little-endian: "TKSPACK1", u32 count, u32 0, then count entries sorted by (kind, name), then the data. name is the
// 32 bytes the loose file's 64 hex digits spell. The index is read once (48 bytes a file: ~5.6 MB after an install);
// the data is read per lookup with positioned reads on one handle, safe from every thread. A pack that fails a check is
// ignored as a whole and the loose files are used.
enum { PACK_SPV = 1, PACK_RES = 2, PACK_USE = 3, PACK_IN = 4 };
typedef struct { uint8_t name[32]; uint32_t kind, size; uint64_t offset; } PackEnt;
_Static_assert(sizeof(PackEnt) == 48, "shader-cache.pack entry layout");
static HANDLE pack_file = INVALID_HANDLE_VALUE;
static PackEnt *pack_ix;
static uint32_t pack_n;
static int pack_cmp(const PackEnt *a, uint32_t kind, const uint8_t name[32]) {
if (a->kind != kind) return a->kind < kind ? -1 : 1;
return memcmp(a->name, name, 32);
}
static int pack_read_at(uint64_t offset, void *out, uint32_t n) {
OVERLAPPED o = {0};
o.Offset = (DWORD)offset, o.OffsetHigh = (DWORD)(offset >> 32);
DWORD got = 0;
return ReadFile(pack_file, out, n, &got, &o) && got == n;
}
// Opens <cache>/shader-cache.pack and reads its index (init_paths). Nothing happens when there is none.
static void pack_open(void) {
char p[PATH_CAP + 32];
wchar_t w[PATH_CAP + 32];
snprintf(p, sizeof p, "%s\\shader-cache.pack", cache_dir);
if (!MultiByteToWideChar(CP_UTF8, 0, p, -1, w, PATH_CAP + 32)) return;
HANDLE h = CreateFileW(w, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) return;
LARGE_INTEGER size;
uint8_t head[16];
uint32_t n = 0;
const char *why = NULL;
PackEnt *ix = NULL;
pack_file = h;
if (!GetFileSizeEx(h, &size) || !pack_read_at(0, head, sizeof head) || memcmp(head, "TKSPACK1", 8)) why = "header";
if (!why) {
memcpy(&n, head + 8, 4);
if (n > (1u << 24) || (uint64_t)n * sizeof *ix + sizeof head > (uint64_t)size.QuadPart ||
!(ix = malloc((size_t)n * sizeof *ix + 1)))
why = "index size";
else if (!pack_read_at(sizeof head, ix, n * (uint32_t)sizeof *ix)) why = "index read";
}
for (uint32_t i = 0; !why && i < n; i++) {
if (ix[i].offset + ix[i].size > (uint64_t)size.QuadPart) why = "entry out of bounds";
else if (i && pack_cmp(&ix[i - 1], ix[i].kind, ix[i].name) >= 0) why = "index order";
}
if (why) {
rt_log("gfx_shader: %s is not a usable cache pack (%s); reading the loose files", p, why);
free(ix);
CloseHandle(h);
pack_file = INVALID_HANDLE_VALUE;
return;
}
pack_ix = ix, pack_n = n;
rt_log("gfx_shader: cache pack %s: %u files", p, n);
}
static const PackEnt *pack_find(uint32_t kind, const uint8_t name[32]) {
size_t lo = 0, hi = pack_n;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
int c = pack_cmp(&pack_ix[mid], kind, name);
if (!c) return &pack_ix[mid];
if (c < 0) lo = mid + 1;
else hi = mid;
}
return NULL;
}
// A packed file as plat_file_read returns a loose one: malloc'd, NUL after the last byte; NULL when absent or empty.
static uint8_t *pack_read(uint32_t kind, const uint8_t name[32], uint64_t *size) {
const PackEnt *e = pack_find(kind, name);
uint8_t *buf = e && e->size ? malloc((size_t)e->size + 1) : NULL;
if (!buf) return NULL;
if (!pack_read_at(e->offset, buf, e->size)) {
free(buf);
return NULL;
}
buf[e->size] = 0;
*size = e->size;
return buf;
}
// Appends a UTF-8 string as a JSON string body: ASCII as is, quotes and backslashes escaped, the rest as \uXXXX
// (UTF-16 units), so the service's stdin encoding does not matter.
static void json_esc(char **o, const char *s) {
@@ -218,6 +311,7 @@ static BOOL CALLBACK init_paths(PINIT_ONCE once, PVOID param, PVOID *ctx) {
if (cache_cfg && *cache_cfg && strlen(cache_cfg) < sizeof cache_dir) strcpy(cache_dir, cache_cfg);
else if (exe_dir[0]) snprintf(cache_dir, sizeof cache_dir, "%s\\shader-cache", exe_dir);
xlate_identity();
if (cache_dir[0]) pack_open();
use_preload_start();
return TRUE;
}
@@ -468,15 +562,15 @@ static int spirv_complete(const uint32_t *w, uint64_t n) {
return i == n;
}
// Reads <key>.spv and <key>.res.json; NULL if either is missing or malformed (the caller translates again and
// overwrites both). iid: the input id, NULL when the caller sets it.
static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
// Reads <key>.spv and <key>.res.json from the pack (packed 1) or the loose files; NULL if either is missing or
// malformed. iid: the input id, NULL when the caller sets it.
static GfxShader *disk_load_from(const uint8_t key[32], const uint8_t *iid, int packed) {
static volatile LONG n_bad;
char hex[65], p[PATH_CAP + 80];
key_hex(key, hex);
uint64_t spv_size = 0, res_size = 0;
snprintf(p, PATH_CAP + 80, "%s\\%s.res.json", cache_dir, hex);
uint8_t *res = read_file(p, &res_size);
uint8_t *res = packed ? pack_read(PACK_RES, key, &res_size) : read_file(p, &res_size);
if (!res) return NULL;
if (!reflection_valid((const char *)res, res_size)) {
LONG n = InterlockedIncrement(&n_bad);
@@ -485,7 +579,7 @@ static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
return NULL;
}
snprintf(p, PATH_CAP + 80, "%s\\%s.spv", cache_dir, hex);
uint8_t *spv = read_file(p, &spv_size);
uint8_t *spv = packed ? pack_read(PACK_SPV, key, &spv_size) : read_file(p, &spv_size);
uint32_t magic = 0;
if (spv && spv_size >= 20) memcpy(&magic, spv, 4);
if (!spv || spv_size % 4 || magic != 0x07230203 || !spirv_complete((const uint32_t *)spv, spv_size / 4)) {
@@ -503,6 +597,13 @@ static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
return s;
}
// The pack, then the loose files (a newer translation of a key the pack holds a bad copy of); NULL when neither has a
// valid pair (the caller translates again and writes loose files).
static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
GfxShader *s = pack_n ? disk_load_from(key, iid, 1) : NULL;
return s ? s : disk_load_from(key, iid, 0);
}
// ---- request building
static const char *const tex_names[] = {NULL, "1d", "1darray", "buffer", "2d", "2darray", "2dms", "2dmsarray", "3d",
@@ -751,10 +852,25 @@ static DWORD WINAPI use_preload(void *arg) {
(void)arg;
uint64_t t0 = plat_now_ns();
char pattern[PATH_CAP + 16], p[PATH_CAP + 80];
int n = 0, bad = 0;
for (uint32_t i = 0; i < pack_n; i++) { // the packed masks; the loose files below add the masks found since
uint32_t u[5];
UseFile **slot, *x;
if (pack_ix[i].kind != PACK_USE) continue;
if (pack_ix[i].size != sizeof u || !pack_read_at(pack_ix[i].offset, u, sizeof u) || u[0] != USE_MAGIC ||
u[1] != USE_VERSION) {
bad++;
continue;
}
if (!*(slot = use_file_slot(pack_ix[i].name)) && (x = malloc(sizeof *x))) {
memcpy(x->name, pack_ix[i].name, 32);
x->next = NULL, x->m.slots = u[2], x->m.attrs = u[3], x->m.fouts = u[4];
*slot = x, n++;
}
}
snprintf(pattern, sizeof pattern, "%s\\*.use", cache_dir);
WIN32_FIND_DATAA fd;
HANDLE f = FindFirstFileExA(pattern, FindExInfoBasic, &fd, FindExSearchNameMatch, NULL, FIND_FIRST_EX_LARGE_FETCH);
int n = 0, bad = 0;
if (f != INVALID_HANDLE_VALUE) {
do {
UseFile e = {0};
@@ -824,7 +940,8 @@ static int use_find(const uint8_t h[32], UseMask *m) {
char p[PATH_CAP + 80];
use_path_of(name, p);
uint64_t n = 0;
uint32_t *f = (uint32_t *)read_file(p, &n);
uint32_t *f = (uint32_t *)pack_read(PACK_USE, name, &n);
if (!f) f = (uint32_t *)read_file(p, &n);
known = f && n == 20 && f[0] == USE_MAGIC && f[1] == USE_VERSION;
if (known) m->slots = f[2], m->attrs = f[3], m->fouts = f[4];
free(f);
@@ -1105,7 +1222,7 @@ static void in_store(const uint8_t iid[32], int stage, const uint8_t *control, c
const GfxShaderAnswers *a) {
char p[PATH_CAP + 80], tmp[PATH_CAP + 120];
in_path(iid, p);
if (file_exists(p)) return;
if (pack_find(PACK_IN, iid) || file_exists(p)) return;
GfxShaderAnswers zero = {0};
InHeader h = {IN_MAGIC, IN_VERSION, (uint32_t)stage, r->length, r->c1_len, sizeof(GfxShaderAnswers)};
size_t n = sizeof h + sizeof zero + CONTROL_SIZE + r->length + r->c1_len;
@@ -1127,7 +1244,8 @@ static int in_load(const uint8_t iid[32], Inputs *in) {
char p[PATH_CAP + 80];
in_path(iid, p);
uint64_t n = 0;
uint8_t *f = read_file(p, &n);
uint8_t *f = pack_read(PACK_IN, iid, &n);
if (!f) f = read_file(p, &n);
InHeader h;
if (!f || n < sizeof h) return free(f), 0;
memcpy(&h, f, sizeof h);
@@ -1425,7 +1543,7 @@ static DWORD WINAPI warm_worker(void *arg) {
key_of_input(iid, key);
key_hex(key, hex);
snprintf(p, PATH_CAP + 80, "%s\\%s.spv", cache_dir, hex);
if (file_exists(p)) InterlockedIncrement(&w->other_cached);
if (pack_find(PACK_SPV, key) || file_exists(p)) InterlockedIncrement(&w->other_cached);
else gfx_shader_load_input(iid, 1);
}
InterlockedIncrement64(&w->done);
@@ -1463,7 +1581,14 @@ static size_t other_inputs(const uint8_t (*named)[32], size_t n_named, uint8_t (
char dir[PATH_CAP + 80];
snprintf(dir, sizeof dir, "%s\\in", cache_dir);
InList l = {.named = named, .n_named = n_named};
plat_file_list(dir, ".in", in_collect, &l);
char name[68];
for (uint32_t i = 0; i < pack_n; i++) { // the packed inputs, under the name their loose file would have
if (pack_ix[i].kind != PACK_IN) continue;
key_hex(pack_ix[i].name, name);
memcpy(name + 64, ".in", 4);
if (!in_collect(name, &l)) break;
}
plat_file_list(dir, ".in", in_collect, &l); // in_store writes no loose file for a packed input: no duplicates
return *out = l.v, l.n;
}
+35 -4
View File
@@ -796,6 +796,27 @@ static void save_capture(const char *path) {
rt_log("gfx: menu test shot %s (%dx%d)", path, g_cap_w, g_cap_h);
}
// Where the game's picture (cw x ch) goes on the sw x sh swapchain: TOTK_ASPECT=fit (default) keeps its shape and centres
// it, with black bars at the sides (a 16:9 picture in a 21:9 window) or above and below; stretch fills the swapchain.
// Bars under a pixel are not drawn. Returns 1 when the rectangle leaves bars to clear.
static int fit_rect(int cw, int ch, int sw, int sh, int out[4]) {
out[0] = 0, out[1] = 0, out[2] = sw, out[3] = sh;
if (cw <= 0 || ch <= 0 || strcmp(cfg_value("TOTK_ASPECT"), "fit")) return 0;
long long a = (long long)cw * sh, b = (long long)ch * sw;
if (a < b) { // narrower than the window: bars at the sides
int fw = (int)((a + ch / 2) / ch);
if (fw >= sw - 1) return 0;
out[0] = (sw - fw) / 2, out[2] = fw;
} else if (a > b) { // wider: bars above and below
int fh = (int)(((long long)ch * sw + cw / 2) / cw);
if (fh >= sh - 1) return 0;
out[1] = (sh - fh) / 2, out[3] = fh;
} else {
return 0;
}
return 1;
}
static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4], int defer) {
int general = layout == VK_IMAGE_LAYOUT_GENERAL;
gfx_window_start();
@@ -855,16 +876,26 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
const int rect[4] = {cx, cy, cw, ch};
// FSR (TOTK_UPSCALER) when the swapchain is larger than the crop; else the bilinear blit
int fsr = general && vk_fsr_record(cb, slot, src, fmt, w, h, rect, dst, g_sc_ext.width, g_sc_ext.height);
g_scaler = fsr ? 2 : (uint32_t)cw != g_sc_ext.width || (uint32_t)ch != g_sc_ext.height;
int to[4];
if (fit_rect(cw, ch, (int)g_sc_ext.width, (int)g_sc_ext.height, to)) {
// black bars: clear the whole image, then the picture's writes below wait for the clear
VkClearColorValue black = {{0, 0, 0, 1}};
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdClearColorImage(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &all);
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT);
}
// FSR (TOTK_UPSCALER) when the picture's place on the swapchain is larger than the crop; else the bilinear blit
int fsr = general && vk_fsr_record(cb, slot, src, fmt, w, h, rect, dst, to);
g_scaler = fsr ? 2 : cw != to[2] || ch != to[3];
if (!fsr) {
if (general) // a GENERAL source (vk_texture.c images): earlier submissions' writes become visible to the blit here
sync_barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
VkImageBlit region = {
.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{cx, cy, 0}, {cx + cw, cy + ch, 1}},
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{0, 0, 0}, {(int)g_sc_ext.width, (int)g_sc_ext.height, 1}}};
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{to[0], to[1], 0}, {to[0] + to[2], to[1] + to[3], 1}}};
vkCmdBlitImage(cb, src, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region, VK_FILTER_LINEAR);
if (general) // later submissions that write the source wait for the blit's read
+3
View File
@@ -242,6 +242,9 @@ void res_heapdef_served(void); // romfs.c serves the
void res_init(void); // after rt_patches_init: the patch page and the sets
void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene sizes in effect (the game's own
// until res_init switched the sets on)
int res_is_16x9(unsigned w, unsigned h); // within 1% of 16:9 (the test res_init and romfs.c share)
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
// ---- code patches (patches.c) and the frame step (frame_rate.c)
void rt_patches_init(void); // after ldr_load: switches from the environment, the patch
// page committed; logs the sets
+11 -2
View File
@@ -44,7 +44,7 @@ static const char *const action_names[ACTION_COUNT] = {
"LSTICK_LEFT", "LSTICK_UP", "LSTICK_RIGHT", "LSTICK_DOWN", "RSTICK_LEFT", "RSTICK_UP", "RSTICK_RIGHT", "RSTICK_DOWN"};
static const char *const pad_names[PAD_COUNT] = {
NULL, "south", "east", "west", "north", "lstick", "rstick", "l", "r", "start", "back",
"dpad_left", "dpad_up", "dpad_right", "dpad_down", "zl", "zr"};
"dpad_left", "dpad_up", "dpad_right", "dpad_down", "zl", "zr", "lpaddle1", "rpaddle1", "lpaddle2", "rpaddle2"};
static const char *const key_names[KEY_COUNT] = { // each group starts at its id; the ids of a group are consecutive
[KEY_A] = "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u",
"v", "w", "x", "y", "z",
@@ -283,7 +283,7 @@ void input_map_line(const InputMap *m, int action, char *out, size_t n) {
}
int input_pad_layout_pick(const char *setting, int east_labelled_a) {
if (setting && !_stricmp(setting, "nintendo")) return PAD_LAYOUT_NINTENDO;
if (setting && (!_stricmp(setting, "nintendo") || !_stricmp(setting, "xbox_prompts"))) return PAD_LAYOUT_NINTENDO;
if (setting && !_stricmp(setting, "xbox")) return PAD_LAYOUT_XBOX;
return east_labelled_a ? PAD_LAYOUT_NINTENDO : PAD_LAYOUT_XBOX;
}
@@ -325,6 +325,15 @@ void input_mouse_stick(int32_t dx, int32_t dy, double dt_s, float sens, int32_t
out[1] = (int32_t)lrint(y);
}
void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]) {
double fx = x / (double)STICK_MAX, fy = y / (double)STICK_MAX, m = sqrt(fx * fx + fy * fy);
out[0] = out[1] = 0;
if (m <= dead) return;
double k = ((m > 1 ? 1 : m) - dead) / (1 - dead) / m; // past the edge (a square gate's corner) the length is 1
out[0] = (int32_t)lrint(fmax(-1, fmin(1, fx * k)) * STICK_MAX);
out[1] = (int32_t)lrint(fmax(-1, fmin(1, fy * k)) * STICK_MAX);
}
// Minus and Plus held together open or close the menu. *held: latched from the poll where both go down until neither is
// down (the caller keeps it). Every poll of the latch reports neither to the game, so releasing one button first does not
// leak the other; Minus alone before Plus reaches the game unchanged. With the menu open the game sees a neutral pad.
+12 -3
View File
@@ -34,11 +34,14 @@ enum {
};
// Gamepad inputs by position (Nintendo layout, as SDL names them), platform-neutral like the key ids. ZL and ZR are the
// analog triggers; input_win.c translates SDL buttons and axes to these (one place).
// analog triggers; input_win.c translates SDL buttons and axes to these (one place). The paddles are the back buttons
// of an Xbox Elite, DualSense Edge or Steam Controller (SDL's LEFT_PADDLE1 and so on); no Switch button has them by
// default, a bind line or the remapping rows add them.
enum {
PAD_NONE = 0,
PAD_SOUTH, PAD_EAST, PAD_WEST, PAD_NORTH, PAD_LSTICK, PAD_RSTICK, PAD_L, PAD_R, PAD_START, PAD_BACK,
PAD_DPAD_LEFT, PAD_DPAD_UP, PAD_DPAD_RIGHT, PAD_DPAD_DOWN, PAD_ZL, PAD_ZR,
PAD_LPADDLE1, PAD_RPADDLE1, PAD_LPADDLE2, PAD_RPADDLE2,
PAD_COUNT
};
@@ -77,8 +80,10 @@ int input_map_bind(const char *button, const char *list);
// Applies the TOTK_BIND_<action> settings (cfg_get) over the defaults: at start, and again when one of them changes.
void input_map_configure(void);
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); anything else ("auto", NULL) picks Nintendo
// when the pad's east button is labelled A (a Switch Pro Controller, a GuliKit) and Xbox otherwise.
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts" is the Nintendo layout with
// the A/B and X/Y prompts swapped (runtime/romfs.c overlay_init), so the buttons map as "nintendo". Anything else
// ("auto", NULL) picks Nintendo when the pad's east button is labelled A (a Switch Pro Controller, a GuliKit) and Xbox
// otherwise.
int input_pad_layout_pick(const char *setting, int east_labelled_a);
// down[pad] != 0 for the gamepad inputs held. Returns the Switch button bits (0..15) the layout's pad table presses.
uint64_t input_map_pad_buttons(const uint8_t down[PAD_COUNT], int layout);
@@ -91,6 +96,10 @@ void input_map_stick(const uint8_t down[KEY_COUNT], int stick, int32_t out[2]);
// Mouse motion in raw counts over dt_s seconds to right-stick deflection: full deflection at 3000 / sens counts/s.
// Clamped to the unit circle, up (negative dy) is +y.
void input_mouse_stick(int32_t dx, int32_t dy, double dt_s, float sens, int32_t out[2]);
// A gamepad stick (x, y in -32768..32767, up is +y) through a round dead zone of `dead` (fraction of full deflection,
// 0..0.5): inside the circle the stick reads 0, and the rest of the travel is scaled to start at 0 at the edge, so a
// drifting stick rests at 0 and full deflection still reaches full. The result stays inside the unit square.
void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]);
// ---- the remapping rows' edits, on a copy of input_map. Pure.
// action's keys (table order) / its gamepad buttons in layout, at most max; returns how many.
+54 -8
View File
@@ -19,7 +19,7 @@
#include "ui/menu.h"
#include "vk.h"
enum { STICK_MAX = 32767, STICK_DEAD = 2500, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
enum { STICK_MAX = 32767, STICK_DIR = 16384, TRIGGER_ON = 16384, RESCAN_MS = 1000 };
static SRWLOCK lock = SRWLOCK_INIT;
static int state; // 0 not started, 1 running, -1 disabled or SDL failed
@@ -43,6 +43,8 @@ static const SDL_GamepadButton pad_sdl[PAD_COUNT] = {
[PAD_R] = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, [PAD_START] = SDL_GAMEPAD_BUTTON_START, [PAD_BACK] = SDL_GAMEPAD_BUTTON_BACK,
[PAD_DPAD_LEFT] = SDL_GAMEPAD_BUTTON_DPAD_LEFT, [PAD_DPAD_UP] = SDL_GAMEPAD_BUTTON_DPAD_UP,
[PAD_DPAD_RIGHT] = SDL_GAMEPAD_BUTTON_DPAD_RIGHT, [PAD_DPAD_DOWN] = SDL_GAMEPAD_BUTTON_DPAD_DOWN,
[PAD_LPADDLE1] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE1, [PAD_RPADDLE1] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1,
[PAD_LPADDLE2] = SDL_GAMEPAD_BUTTON_LEFT_PADDLE2, [PAD_RPADDLE2] = SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2,
};
// TOTK_INPUT_SCRIPT=<file>: lines "<frame> <button> [hold]" or "<frame> LSTICK|RSTICK <x> <y> [hold]", ORed into the real input.
@@ -428,9 +430,21 @@ static void mouse_camera(int32_t out[2]) {
out[1] = held[1];
}
static int32_t axis(int16_t v) {
int32_t a = v < -STICK_MAX ? -STICK_MAX : v;
return a > -STICK_DEAD && a < STICK_DEAD ? 0 : a;
static float stick_dead = -1; // TOTK_STICK_DEADZONE as a fraction of full deflection; live (cfg_version)
static unsigned stick_dead_seen;
static float stick_dead_get(void) {
unsigned ver = cfg_version();
if (stick_dead >= 0 && ver == stick_dead_seen) return stick_dead;
stick_dead_seen = ver;
double v = cfg_double("TOTK_STICK_DEADZONE");
stick_dead = (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
return stick_dead;
}
// One gamepad stick through the dead zone; SDL's Y grows downwards, the Switch's upwards.
static void stick(SDL_GamepadAxis ax, SDL_GamepadAxis ay, float dead, int32_t out[2]) {
input_pad_stick(SDL_GetGamepadAxis(pad, ax), -(int32_t)SDL_GetGamepadAxis(pad, ay), dead, out);
}
// Open the first gamepad when none is open or the open one went away; logs once per change.
@@ -503,12 +517,44 @@ static void binds_refresh(void) {
input_map_configure();
}
// TOTK_TEST_PAD=<lx>,<ly>,<rx>,<ry> (test runs): an SDL virtual gamepad whose sticks rest at these raw SDL axis values
// (-32768..32767, down is +y), attached before the first scan so it is the gamepad the game reads: drift and full
// deflection go through the same mapping and dead zone as a real pad.
static void test_pad_attach(void) {
static SDL_Joystick *joy; // kept open: a virtual joystick's axes are set through an open handle
const char *e = cfg_get("TOTK_TEST_PAD");
int v[4];
if (!e || !*e) return;
if (sscanf(e, "%d,%d,%d,%d", &v[0], &v[1], &v[2], &v[3]) != 4) {
rt_log("input: TOTK_TEST_PAD=%s is not <lx>,<ly>,<rx>,<ry>; no test gamepad", e);
return;
}
SDL_VirtualJoystickDesc d;
SDL_INIT_INTERFACE(&d);
d.type = SDL_JOYSTICK_TYPE_GAMEPAD;
d.naxes = SDL_GAMEPAD_AXIS_COUNT;
d.nbuttons = SDL_GAMEPAD_BUTTON_COUNT;
d.name = "TotK test gamepad";
SDL_JoystickID id = SDL_AttachVirtualJoystick(&d);
joy = id ? SDL_OpenJoystick(id) : NULL;
if (!joy) {
rt_log("input: test gamepad not attached (%s)", SDL_GetError());
return;
}
static const SDL_GamepadAxis axes[4] = {SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_RIGHTX,
SDL_GAMEPAD_AXIS_RIGHTY};
for (int i = 0; i < 4; i++)
SDL_SetJoystickVirtualAxis(joy, axes[i], (Sint16)(v[i] < -32768 ? -32768 : v[i] > 32767 ? 32767 : v[i]));
rt_log("input: test gamepad attached, sticks at %d,%d %d,%d", v[0], v[1], v[2], v[3]);
}
static void start(void) {
binds_refresh();
const char *e = cfg_get("TOTK_INPUT");
if (e && !strcmp(e, "none")) { state = -1; rt_log("input: disabled (TOTK_INPUT=none)"); return; }
if (!SDL_Init(SDL_INIT_GAMEPAD)) { state = -1; rt_log("input: SDL gamepad init failed (%s), keyboard only", SDL_GetError()); }
else state = 1;
if (state == 1) test_pad_attach();
if (state == 1) {
scan_pads();
if (!pad) rt_log("input: no gamepad (keyboard only until one is connected)");
@@ -536,6 +582,7 @@ static int pad_physical(uint8_t pd[PAD_COUNT]) {
for (int p = PAD_SOUTH; p < PAD_ZL; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]);
pd[PAD_ZL] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_ON;
pd[PAD_ZR] = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_ON;
for (int p = PAD_ZR + 1; p < PAD_COUNT; p++) pd[p] = SDL_GetGamepadButton(pad, pad_sdl[p]); // false without paddles
return 1;
}
@@ -545,10 +592,9 @@ static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
uint8_t pd[PAD_COUNT] = {0};
if (!pad_physical(pd)) return 0;
*b |= input_map_pad_buttons(pd, pad_layout_get());
sl[0] = axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX));
sl[1] = -axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY));
sr[0] = axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTX));
sr[1] = -axis(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTY));
float dead = stick_dead_get();
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, dead, sl);
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, dead, sr);
return 1;
}
+9 -4
View File
@@ -7,8 +7,9 @@
// axis, direction = the controller's three axes in a reference frame whose z is up. The orientation is integrated from
// the gyro and, while the controller is not being accelerated (|a| within 15 % of 1 G), pulled toward the measured
// gravity (Mahony, gain 0.5/s: about 2 s), as the Switch's sensor fusion does; yaw has no reference and drifts with the
// gyro's bias. The newest MOTION_RING samples are kept; reads are newest first. The feeding thread writes, guest
// threads read, both under the instance's SRW lock.
// gyro's bias. The newest MOTION_RING samples are kept; reads are newest first. A restart (StartSixAxisSensor) drops
// the samples before it: reads return nothing until the next sample, which starts a fresh frame and angle; sampling
// numbers go on. The feeding thread writes, guest threads read, both under the instance's SRW lock.
#include <windows.h>
#include <math.h>
#include <stdlib.h>
@@ -24,6 +25,7 @@ struct MotionFusion {
SRWLOCK lock;
MotionState ring[MOTION_RING];
uint64_t produced, last_ns;
uint64_t first; // produced at the last restart: older samples are not read
double R[3][3]; // controller -> reference; row i = reference axis i in controller coordinates
double angle[3];
int restart;
@@ -131,8 +133,8 @@ int motion_read(MotionFusion *m, MotionState *out, int max, uint64_t now) {
if (max <= 0) return 0;
int n = 0;
AcquireSRWLockShared(&m->lock);
if (m->produced && (now < m->last_ns || now - m->last_ns < STALE_NS)) {
uint64_t have = m->produced < MOTION_RING ? m->produced : MOTION_RING;
if (m->produced > m->first && (now < m->last_ns || now - m->last_ns < STALE_NS)) {
uint64_t have = m->produced - m->first < MOTION_RING ? m->produced - m->first : MOTION_RING;
n = (int)(have < (uint64_t)max ? have : (uint64_t)max);
for (int i = 0; i < n; i++) out[i] = m->ring[(m->produced - 1 - (uint64_t)i) % MOTION_RING];
}
@@ -143,5 +145,8 @@ int motion_read(MotionFusion *m, MotionState *out, int max, uint64_t now) {
void motion_restart(MotionFusion *m) {
AcquireSRWLockExclusive(&m->lock);
m->restart = 1;
m->first = m->produced;
m->last_ns = 0; // the first sample after the restart integrates nothing across the stop
memset(m->angle, 0, sizeof m->angle);
ReleaseSRWLockExclusive(&m->lock);
}
+13 -1
View File
@@ -552,9 +552,21 @@ static void probe_feedback(uint32_t pid, uint32_t style) {
near3(ms[0].dir + 3, -1, 0, 0, 0.01) && near3(ms[0].dir + 6, 0, 0, 1, 0.01),
"fusion quarter turn: %d states, sampling %lld, angle %.4f rev, x axis (%.3f %.3f %.3f)", n,
(long long)ms[0].sampling, (double)ms[0].angle[2], (double)ms[0].dir[0], (double)ms[0].dir[1], (double)ms[0].dir[2]);
double quarter = ms[0].angle[2];
CHECK(motion_read(m, ms, 16, t + 300000000ull) == 0, "fusion states 300 ms after the last sample");
// StartSixAxisSensor again in the middle of the turn (the bow drawn again): the states from before it are gone, and
// the first one after it starts a fresh angle in a fresh frame; sampling numbers go on.
for (int i = 0; i < 250; i++) motion_feed(m, down, turn, t += 4000000);
motion_restart(m);
int same = motion_read(m, ms, 16, t + 1000000);
motion_feed(m, down, turn, t += 4000000);
int next = motion_read(m, ms, 16, t + 1000000);
CHECK(same == 0 && next == 1 && ms[0].sampling == 502 && fabs(ms[0].angle[2]) < 0.002 &&
near3(ms[0].dir, 1, 0, 0, 0.01),
"fusion restart: %d states in the frame of the Start, %d after one sample (sampling %lld, angle %.4f rev)", same,
next, (long long)ms[0].sampling, (double)ms[0].angle[2]);
printf("hid feedback: vibration handles %#x %#x, six-axis %#x, at rest when stopped; fusion quarter turn %.4f rev\n",
vh[0], vh[1], sh[0], (double)ms[0].angle[2]);
vh[0], vh[1], sh[0], quarter);
}
static void probe_input(void) {
+3
View File
@@ -116,6 +116,9 @@ void plat_cpu_load(PlatLoad *l, float *total_pct, float *proc_pct, float *temp_c
void plat_gpu_load(PlatLoad *l, float *load_pct, float *vram_used_mb);
// Physical memory in MB: in use and installed on the machine, and this process's working set.
void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb);
// Commit charge in MB: this process's private bytes, the system's commit in use and its limit (RAM plus page files).
// An allocation fails when the system's reaches the limit, however much physical memory is free. -1 when unknown.
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb);
// ---- child processes: a program with stdin and stdout piped to us (stderr discarded), no console window.
typedef struct PlatChild PlatChild;
+12
View File
@@ -613,6 +613,18 @@ void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb) {
if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc)) *proc_mb = (float)(pmc.WorkingSetSize >> 20);
}
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
*proc_mb = *used_mb = *limit_mb = -1;
MEMORYSTATUSEX ms = {.dwLength = sizeof ms};
if (GlobalMemoryStatusEx(&ms)) {
*limit_mb = (double)(ms.ullTotalPageFile >> 20);
*used_mb = (double)((ms.ullTotalPageFile - ms.ullAvailPageFile) >> 20);
}
PROCESS_MEMORY_COUNTERS_EX pmc;
if (K32GetProcessMemoryInfo(GetCurrentProcess(), (PROCESS_MEMORY_COUNTERS *)&pmc, sizeof pmc))
*proc_mb = (double)(pmc.PrivateUsage >> 20);
}
// ---- child processes
struct PlatChild {
+36
View File
@@ -12,6 +12,7 @@
//
// Patch page: +16 the scene size {w, h} as floats, +32 the output size and +48 the swapchain size as {w, h} in one
// 64-bit word, +40 the renderer (1.2.1) or display configuration (1.4.3) the patched code stores there.
// +56 the camera's aspect ratio as a float when the output is not 16:9 (1.4.3, set aspect).
// 1.4.3 scales the scene it reads by output width / 1920 (a 2560x1440 output renders its 1600x900 scene at 2128x1200;
// MacNativeGames measured the same), so its page holds the wanted size divided by that factor and the game's 5/6 needs
// no scene patch. 1.2.1 renders the size it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and
@@ -218,10 +219,45 @@ void res_init(void) {
rt_patch_switch(output, R.ow != BASE_W || R.oh != BASE_H);
rt_patch_switch(scene, scene_on);
R.on = 1;
// Another shape than 16:9 (more than 1% off): the camera takes the output's ratio (recomp/patches-v143.txt, set
// aspect), so a 21:9 output shows more of the world at the sides instead of a stretched 16:9 view. The HUD's layouts
// for that shape come from romfs-patches-<w>x<h> (romfs.c overlay_init, tools/make_romfs_patches.py --aspect).
double ratio = (double)R.ow / R.oh, game = (double)BASE_W / BASE_H;
int aspect = rt_patch_set("aspect");
if (!res_is_16x9(R.ow, R.oh)) {
if (aspect < 0) {
rt_log("resolution: output %ux%u is not 16:9 and this game version has no camera aspect patch; the view "
"is stretched", R.ow, R.oh);
} else {
float r = (float)ratio;
memcpy(page + 56, &r, sizeof r);
rt_patch_switch(aspect, 1);
rt_log("resolution: camera aspect ratio %.4f (16:9 is %.4f)", ratio, game);
}
} else {
rt_patch_switch(aspect, 0); // the page holds no ratio
}
rt_log("resolution: output %ux%u, scene %ux%u%s; texture heap +%llu KiB", R.ow, R.oh, R.sw, R.sh,
scene_on ? "" : " (the game's own 5/6 of the output)", (unsigned long long)res_heap_extra_kib());
}
// 16:9 within 1% (1366x768 counts): the shape the game's own layouts and camera are made for.
int res_is_16x9(unsigned w, unsigned h) {
double rel = ((double)w / h) / ((double)BASE_W / BASE_H);
return rel >= 0.99 && rel <= 1.01;
}
// The game resolution asks for a romfs-patches-<w>x<h> overlay: it is not 16:9 and the game version has the camera
// aspect patch (set aspect, 1.4.3 only); the wide UI layouts only fit a camera that follows the same shape.
int res_wants_wide_overlay(const unsigned out[2]) {
return !res_is_16x9(out[0], out[1]) && rt_patch_set("aspect") >= 0;
}
void res_wanted(unsigned out[2]) {
decide();
out[0] = R.ow, out[1] = R.oh;
}
void res_sizes(unsigned out[2], unsigned scene[2]) {
decide();
out[0] = R.on ? R.ow : BASE_W, out[1] = R.on ? R.oh : BASE_H;
+73 -16
View File
@@ -244,7 +244,11 @@ static uint32_t overlay_slot(uint64_t i) {
}
// ---- overlays
static void overlay_load(const char *dir) {
// Loads and checks one overlay folder. A failed check stops the game, except with optional set (the Xbox prompts
// folder): the folder is dropped, the reason logged, and 0 returned. Returns 1 when the folder is loaded or has no
// manifest.
static int overlay_load(const char *dir, int optional) {
#define BAD(...) do { if (!optional) rt_fatal(__VA_ARGS__); rt_log(__VA_ARGS__); goto bad; } while (0)
char path[2100];
need(snprintf(path, sizeof path, "%s/manifest.tsv", dir) < (int)sizeof path, "overlay path");
wchar_t *w = fs_wide(path);
@@ -252,7 +256,7 @@ static void overlay_load(const char *dir) {
free(w);
if (!m) {
rt_log("RomFS overlay: none (%s)", path);
return;
return 1;
}
char line[8192];
size_t first = noverlay;
@@ -268,30 +272,32 @@ static void overlay_load(const char *dir) {
uint64_t i = lower(name);
int known = i < file_count && !strcmp(entry_name(entry(i)), name), add = !strcmp(orig, "-");
if (!known && !add)
rt_fatal("RomFS overlay: %s/manifest.tsv names %s, which the RomFS lacks; a new file has \"-\" as its "
"original's SHA-256", dir, name);
BAD("RomFS overlay: %s/manifest.tsv names %s, which the RomFS lacks; a new file has \"-\" as its "
"original's SHA-256", dir, name);
if (known && add)
rt_fatal("RomFS overlay: %s/manifest.tsv adds %s, which the RomFS already has; give its original's SHA-256",
dir, name);
BAD("RomFS overlay: %s/manifest.tsv adds %s, which the RomFS already has; give its original's SHA-256",
dir, name);
Entry e = {0};
if (known) {
e = entry(i);
sha_range(image, e.offset, e.size, hex, 0);
if (strcmp(hex, orig)) rt_fatal("RomFS overlay: the original of %s has SHA-256 %s, %s/manifest.tsv says %s",
name, hex, dir, orig);
if (strcmp(hex, orig)) BAD("RomFS overlay: the original of %s has SHA-256 %s, %s/manifest.tsv says %s",
name, hex, dir, orig);
} else {
i = ADDED;
}
char file[8200];
need(snprintf(file, sizeof file, "%s/%s", dir, name) < (int)sizeof file, "overlay file path");
HANDLE f = open_read(file, 1);
if (f == INVALID_HANDLE_VALUE) rt_fatal("RomFS overlay: %s missing", file);
if (f == INVALID_HANDLE_VALUE) BAD("RomFS overlay: %s missing", file);
Identity id = identity_of(f);
if (id.size != size) rt_fatal("RomFS overlay: %s has %llu bytes, its manifest says %llu", file,
(unsigned long long)id.size, size);
if (id.size != size) {
CloseHandle(f);
BAD("RomFS overlay: %s has %llu bytes, its manifest says %llu", file, (unsigned long long)id.size, size);
}
sha_range(f, 0, size, hex, 0);
CloseHandle(f);
if (strcmp(hex, sha)) rt_fatal("RomFS overlay: %s has SHA-256 %s, its manifest says %s", file, hex, sha);
if (strcmp(hex, sha)) BAD("RomFS overlay: %s has SHA-256 %s, its manifest says %s", file, hex, sha);
hashed += e.size + size;
if (noverlay == overlay_cap) {
overlay_cap = overlay_cap ? overlay_cap * 2 : 16;
@@ -309,6 +315,16 @@ static void overlay_load(const char *dir) {
noverlay - first - 16);
if (noverlay > first) rt_log("RomFS overlay: %s checked, %zu files, %.1f MiB hashed in %.2f s", dir,
noverlay - first, hashed / 1048576.0, now_s() - t0);
return 1;
bad:
fclose(m);
for (size_t k = first; k < noverlay; k++) {
free((void *)overlays[k].path);
free((void *)overlays[k].name);
}
noverlay = first;
return 0;
#undef BAD
}
static int overlay_order(const void *a, const void *b) {
@@ -407,18 +423,59 @@ static void heapdef_overlay(void) {
res_heapdef_served();
}
static int xbox_prompts_ready; // romfs-patches-xbox/manifest.tsv was next to romfs-index.bin at start
static int xbox_prompts_active; // and overlay_init loaded it
int romfs_xbox_prompts_ready(void) { return xbox_prompts_ready; }
int romfs_xbox_prompts_active(void) { return xbox_prompts_active; }
static void overlay_init(void) {
// TOTK_ROMFS_PATCHES unset: romfs-patches next to romfs-index.bin if it has a manifest (tools/make_romfs_patches.py)
char def[2100];
// TOTK_ROMFS_PATCHES unset: romfs-patches next to romfs-index.bin if it has a manifest (tools/make_romfs_patches.py).
// A game resolution that is not 16:9 takes romfs-patches-<w>x<h> instead when it is there: the same overlay with
// the UI laid out for that shape (make_romfs_patches.py --aspect). 1.2.1 has no camera aspect patch, so it keeps
// the 16:9 layout (res_wants_wide_overlay).
// With pad_layout = xbox_prompts, romfs-patches-xbox in its place: the same patches with the A/B and X/Y prompts
// swapped (make_romfs_patches.py --xbox-prompts). The game reads the archives once at boot, so the prompts stay
// until restart even though the layout itself changes live. Each folder replaces romfs-patches as a whole and the
// setup builds no wide folder with Xbox prompts, so a wide game resolution keeps the Switch prompts.
char def[2100], xbox[2100];
const char *patches = cfg_get("TOTK_ROMFS_PATCHES");
if (!patches && snprintf(def, sizeof def, "%s/romfs-patches/manifest.tsv", index_dir) < (int)sizeof def &&
int wide = 0;
unsigned out[2];
res_wanted(out);
if (!patches && res_wants_wide_overlay(out)) {
if (snprintf(def, sizeof def, "%s/romfs-patches-%ux%u/manifest.tsv", index_dir, out[0], out[1]) < (int)sizeof def &&
GetFileAttributesA(def) != INVALID_FILE_ATTRIBUTES) {
def[strlen(def) - strlen("/manifest.tsv")] = 0;
patches = def, wide = 1;
} else {
rt_log("RomFS overlay: no romfs-patches-%ux%u for this game resolution, the UI keeps its 16:9 layout "
"(tools/make_romfs_patches.py --aspect %ux%u)", out[0], out[1], out[0], out[1]);
}
}
xbox_prompts_ready =
snprintf(xbox, sizeof xbox, "%s/romfs-patches-xbox/manifest.tsv", index_dir) < (int)sizeof xbox &&
GetFileAttributesA(xbox) != INVALID_FILE_ATTRIBUTES;
int want_xbox = !_stricmp(cfg_value("TOTK_PAD_LAYOUT"), "xbox_prompts");
if (!patches && want_xbox && xbox_prompts_ready) {
xbox[strlen(xbox) - strlen("/manifest.tsv")] = 0;
xbox_prompts_active = overlay_load(xbox, 1);
if (xbox_prompts_active) rt_log("RomFS overlay: Xbox button prompts (%s)", xbox);
else rt_log("RomFS overlay: %s failed its check (run the setup again); romfs-patches loads instead", xbox);
} else if (want_xbox) {
rt_log("RomFS overlay: pad_layout = xbox_prompts, but %s; Switch prompts stay",
wide ? "the game resolution takes the wide UI layout"
: patches ? "TOTK_ROMFS_PATCHES is set"
: "romfs-patches-xbox has no manifest.tsv (run the setup again)");
}
if (!patches && !xbox_prompts_active && snprintf(def, sizeof def, "%s/romfs-patches/manifest.tsv", index_dir) < (int)sizeof def &&
GetFileAttributesA(def) != INVALID_FILE_ATTRIBUTES) {
def[strlen(def) - strlen("/manifest.tsv")] = 0;
patches = def;
}
const char *dirs[2] = {patches, cfg_get("TOTK_ROMFS_MODS")};
for (int i = 0; i < 2; i++)
if (dirs[i] && *dirs[i] && strcmp(dirs[i], "0")) overlay_load(dirs[i]);
if (dirs[i] && *dirs[i] && strcmp(dirs[i], "0")) overlay_load(dirs[i], 0);
heapdef_overlay();
added_init();
if (noverlay) qsort(overlays, noverlay, sizeof *overlays, overlay_order);
+89
View File
@@ -0,0 +1,89 @@
// RGBA8 texels to BC3 blocks on the GPU, one invocation per 4x4 block: the second pass of the ASTC decode when
// TOTK_ASTC_BC is on (vk_astc.c), so a decoded ASTC texture takes 8 bits per texel in video memory instead of 32.
// Texels past the right or bottom edge repeat the last column or row.
//
// Colour: the BC1 four-colour block of the box around the block's colours, its diagonal turned to the sign of the
// colours' covariance and inset by 1/16 of the range (the bounding-box method of stb_dxt and of yuzu's BC1 recompression),
// then each texel takes the nearest of the four palette entries along that axis. Alpha: the eight-value BC3 alpha
// block between the block's smallest and largest alpha. The bytes are what the texels hold, so an sRGB image keeps
// sRGB-encoded values, as BC3_SRGB expects.
// Compiled to runtime/bc3_encode_spv.h by tools/build_shaders.py; vk_astc.c records the dispatch.
#version 450
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(std430, set = 0, binding = 0) readonly buffer Texels { uint px[]; };
layout(std430, set = 0, binding = 1) writeonly buffer Blocks { uvec4 blk[]; };
layout(push_constant) uniform Params {
uint in_base; // first texel of slice 0 (index into px)
uint out_base; // first block of slice 0 (uvec4 index into blk)
uint width, height;
uint bx; // blocks per row
uint bslice; // blocks per slice
} pc;
vec4 texel(uint x, uint y, uint z) {
x = min(x, pc.width - 1u), y = min(y, pc.height - 1u);
return unpackUnorm4x8(px[pc.in_base + (z * pc.height + y) * pc.width + x]) * 255.0;
}
uint to565(vec3 c) {
uvec3 q = uvec3(round(clamp(c, 0.0, 255.0) * vec3(31.0, 63.0, 31.0) / 255.0));
return q.r << 11 | q.g << 5 | q.b;
}
vec3 from565(uint v) {
return vec3(float(v >> 11 & 31u) * 255.0 / 31.0, float(v >> 5 & 63u) * 255.0 / 63.0, float(v & 31u) * 255.0 / 31.0);
}
void main() {
uvec3 id = gl_GlobalInvocationID;
uint by = (pc.height + 3u) / 4u;
if (id.x >= pc.bx || id.y >= by) return;
vec4 t[16];
vec4 lo = vec4(255.0), hi = vec4(0.0), mean = vec4(0.0);
for (uint i = 0u; i < 16u; i++) {
t[i] = texel(id.x * 4u + (i & 3u), id.y * 4u + (i >> 2), id.z);
lo = min(lo, t[i]), hi = max(hi, t[i]), mean += t[i];
}
mean /= 16.0;
// alpha: a0 > a1 selects the eight-value mode; index 0 is a0, 1 is a1, 2..7 the six steps from a0 to a1
uint a0 = uint(hi.a), a1 = uint(lo.a);
uvec2 ab = uvec2(a0 | a1 << 8, 0u);
if (a0 > a1) {
float range = float(a0 - a1);
for (uint i = 0u; i < 16u; i++) {
uint s = uint(clamp(round((float(a0) - t[i].a) * 7.0 / range), 0.0, 7.0)); // 0 = a0 .. 7 = a1
uint code = s == 0u ? 0u : s == 7u ? 1u : s + 1u;
uint bit = 16u + 3u * i;
if (bit < 32u) ab.x |= code << bit;
if (bit + 3u > 32u) ab.y |= code >> (32u - min(bit, 32u)) << (bit > 32u ? bit - 32u : 0u);
}
}
// colour: the box diagonal along the covariance of red and blue with green
vec3 cmin = lo.rgb, cmax = hi.rgb;
float crg = 0.0, cbg = 0.0;
for (uint i = 0u; i < 16u; i++) {
vec3 d = t[i].rgb - mean.rgb;
crg += d.r * d.g, cbg += d.b * d.g;
}
if (crg < 0.0) { float s = cmin.r; cmin.r = cmax.r; cmax.r = s; }
if (cbg < 0.0) { float s = cmin.b; cmin.b = cmax.b; cmax.b = s; }
vec3 inset = (cmax - cmin) / 16.0;
uint c0 = to565(cmax - inset), c1 = to565(cmin + inset);
uint idx = 0u;
if (c0 != c1) {
if (c0 < c1) { uint s = c0; c0 = c1; c1 = s; } // c0 > c1: the four-colour mode
vec3 e0 = from565(c0), e1 = from565(c1), axis = e1 - e0;
float len2 = max(dot(axis, axis), 1e-6);
for (uint i = 0u; i < 16u; i++) {
uint s = uint(clamp(round(dot(t[i].rgb - e0, axis) / len2 * 3.0), 0.0, 3.0)); // 0 = e0 .. 3 = e1
uint code = s == 0u ? 0u : s == 3u ? 1u : s + 1u; // palette order e0, e1, 2/3 e0 + 1/3 e1, 1/3 e0 + 2/3 e1
idx |= code << (2u * i);
}
}
blk[pc.out_base + id.z * pc.bslice + id.y * pc.bx + id.x] = uvec4(ab.x, ab.y, c0 | c1 << 16, idx);
}
+1
View File
@@ -54,6 +54,7 @@ typedef struct TexVkFormat {
uint8_t astc_bw, astc_bh; // nonzero: ASTC decoded into vk (RGBA8, same colour space) by vk_astc.c or tex_astc.c;
// zero for native ASTC (vk == ideal, raw blocks as for BC)
uint8_t astc_srgb;
uint8_t astc_bc; // ASTC decoded and then encoded to BC3 (vk BC3, TOTK_ASTC_BC; vk_astc.c)
uint8_t depth, stencil; // aspect flags of depth / stencil formats
const char *note; // why a fallback or an expansion was chosen; NULL when the mapping is direct
const char *reason; // unmapped formats only: why there is no Vulkan format
+2 -1
View File
@@ -21,7 +21,8 @@ void rt_log(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
static_assert(UI_BIND_RSTICK_DOWN - UI_BIND_A == ACTION_COUNT - 1, "UI_BIND_* follow the actions");
static_assert(UI_BIND_DESC_MINUS - UI_BIND_DESC_A == B_MINUS, "UI_BIND_DESC_* follow the actions");
static_assert(UI_KEY_PAGEDOWN - UI_KEY_SPACE == KEY_PAGEDOWN - KEY_SPACE, "UI_KEY_* follow the key ids");
static_assert(UI_PAD_ZR - UI_PAD_SOUTH == PAD_ZR - PAD_SOUTH, "UI_PAD_* follow the pad ids");
static_assert(UI_PAD_RPADDLE2 - UI_PAD_SOUTH == PAD_RPADDLE2 - PAD_SOUTH && PAD_RPADDLE2 == PAD_COUNT - 1,
"UI_PAD_* follow the pad ids");
enum { CAPTURE_SECONDS = 5 };
// The capture window stays this long after the press: the press and its click reach ImGui a frame or two later and must
+21 -5
View File
@@ -38,6 +38,9 @@ void vk_queue_unlock(void);
void res_sizes(unsigned out[2], unsigned scene[2]);
double frame_step_limit_hz(void);
extern const int rt_game_version;
// fs.h (romfs.c): romfs-patches-xbox was next to romfs-index.bin at start, and was loaded.
int romfs_xbox_prompts_ready(void);
int romfs_xbox_prompts_active(void);
extern const char rt_port_version[];
}
@@ -414,10 +417,12 @@ static void row_window_size(void) {
opt_choice_list("TOTK_WINDOW_SIZE", UI_MENU_WINDOW_SIZE, pv, pn, n, UI_MENU_WINDOW_SIZE_DESC);
}
// The game resolution (resolution.c): 16:9 sizes from the game's own 1920x1080 up, and the current value when it is
// The game resolution (resolution.c): 16:9 sizes from its smallest, 1280x720 (for weak GPUs), up, the common 21:9
// monitor sizes (the camera and the HUD follow a wider shape, resolution.c res_init), and the current value when it is
// another size.
static void row_output_res(void) {
static const char *const vals[] = {"1920x1080", "2560x1440", "3200x1800", "3840x2160"};
static const char *const vals[] = {"1280x720", "1600x900", "1920x1080", "2560x1440", "3200x1800", "3840x2160",
"2560x1080", "3440x1440", "3840x1600", "5120x2160"};
enum { N = sizeof vals / sizeof *vals };
char shown[N][32];
const char *names[N];
@@ -534,6 +539,8 @@ static void tab_screen(float scale) {
static const UiTextId up[] = {UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR};
opt_choice("TOTK_UPSCALER", UI_MENU_UPSCALER, up, 2, UI_MENU_UPSCALER_DESC);
opt_slider("TOTK_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
static const UiTextId aspect[] = {UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH};
opt_choice("TOTK_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
opt_end();
}
@@ -555,8 +562,14 @@ static void tab_sound(float scale) {
opt_end();
}
// "in use: Xbox" under the layout when it is Automatic and a gamepad is connected; NULL otherwise.
// The note under the layout. Xbox prompts (the Nintendo layout with the A/B and X/Y prompts swapped, romfs.c
// overlay_init) change the buttons at once but the prompts only at the next start: "files missing" without
// romfs-patches-xbox, "prompts change after restart" while the prompts in use differ from the choice. Automatic with a
// gamepad connected: "in use: Xbox". NULL otherwise.
static const char *pad_layout_note(void) {
bool want = !_stricmp(cfg_value("TOTK_PAD_LAYOUT"), "xbox_prompts");
if (want && !romfs_xbox_prompts_ready()) return menu_text(UI_MENU_PAD_LAYOUT_PROMPTS_MISSING);
if (want != (romfs_xbox_prompts_active() != 0)) return menu_text(UI_MENU_PAD_LAYOUT_PROMPTS_RESTART);
int got = input_pad_layout();
if (got < 0 || strcmp(cfg_value("TOTK_PAD_LAYOUT"), "auto")) return nullptr;
static char note[96];
@@ -567,12 +580,14 @@ static const char *pad_layout_note(void) {
static void tab_controls(float scale) {
opt_begin("##controls", scale);
static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX};
opt_choice("TOTK_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 3, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());
static const UiTextId layout[] = {UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO, UI_MENU_PAD_LAYOUT_XBOX,
UI_MENU_PAD_LAYOUT_XBOX_PROMPTS};
opt_choice("TOTK_PAD_LAYOUT", UI_MENU_PAD_LAYOUT, layout, 4, UI_MENU_PAD_LAYOUT_DESC, pad_layout_note());
opt_toggle("TOTK_MOTION", UI_MENU_MOTION, UI_MENU_MOTION_DESC);
opt_slider("TOTK_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%");
opt_slider("TOTK_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f");
opt_toggle("TOTK_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC);
opt_slider("TOTK_STICK_DEADZONE", UI_MENU_STICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%");
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
binds_rows(scale);
opt_end();
@@ -670,6 +685,7 @@ static void tab_advanced(float scale) {
opt_choice("TOTK_MEMORY", UI_MENU_MEMORY, mem, 4, UI_MENU_MEMORY_DESC);
opt_toggle("TOTK_TEX_ALIAS", UI_MENU_TEX_ALIAS, UI_MENU_TEX_ALIAS_DESC);
opt_slider("TOTK_TEX_VRAM_PRESSURE", UI_MENU_TEX_VRAM_PRESSURE, 0, 100, UI_MENU_TEX_VRAM_PRESSURE_DESC, 1, "%.0f%%");
opt_toggle("TOTK_ASTC_BC", UI_MENU_ASTC_BC, UI_MENU_ASTC_BC_DESC);
opt_group(UI_MENU_GROUP_FIXES);
opt_toggle("TOTK_LOD_GUARD", UI_MENU_LOD_GUARD, UI_MENU_LOD_GUARD_DESC);
+39 -6
View File
@@ -52,7 +52,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_GRAPHICS] = "Picture",
[UI_MENU_OUTPUT_RES] = "Game resolution",
[UI_MENU_OUTPUT_RES_DESC] = "The size the game draws its picture and interface in. Above 1920 x 1080 the game needs "
"more video memory and GPU time.",
"more video memory and GPU time. A wider size (21:9) widens the camera's view and moves "
"the HUD to the screen's corners.",
[UI_MENU_RENDER_SCALE] = "3D render scale",
[UI_MENU_RENDER_SCALE_DESC] = "Size of the 3D scene relative to the game resolution; the game's FSR scales it to "
"that size. Above 100 % smooths jagged edges (supersampling) at a high GPU cost.",
@@ -61,6 +62,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_UPSCALER_DESC] = "How the picture is scaled when the window is larger than the game resolution. FSR keeps "
"edges and detail sharper than bilinear.",
[UI_MENU_UPSCALER_FSR] = "AMD FSR 1", [UI_MENU_UPSCALER_BILINEAR] = "Bilinear",
[UI_MENU_ASPECT] = "Picture shape",
[UI_MENU_ASPECT_DESC] = "When the window's shape differs from the game resolution's (a 16:9 picture on a 21:9 "
"screen): keep the picture's shape with black bars, or stretch it over the window.",
[UI_MENU_ASPECT_FIT] = "Black bars", [UI_MENU_ASPECT_STRETCH] = "Stretch",
[UI_MENU_SHARPNESS] = "FSR sharpness",
[UI_MENU_SHARPNESS_DESC] = "Sharpening after FSR scaling; 0 turns it off.",
@@ -94,9 +99,17 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_PAD_LAYOUT] = "Button layout",
[UI_MENU_PAD_LAYOUT_DESC] = "Which button is A. Nintendo: the right one, as on a Switch. Xbox: the bottom one, as "
"labelled on an Xbox controller (cross on a PlayStation controller). Automatic picks "
"Nintendo for controllers with Nintendo labels and Xbox for the others.",
"Nintendo for controllers with Nintendo labels and Xbox for the others. Xbox prompts: "
"A is the right one as with Nintendo, and the game shows the Xbox labels (B for A, A "
"for B, Y for X, X for Y); the prompts change after a restart. The letters are part of the "
"game's files, so a button you remap in Controls keeps the old letter on screen.",
[UI_MENU_PAD_LAYOUT_AUTO] = "Automatic", [UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo (A right)",
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A bottom)", [UI_MENU_PAD_LAYOUT_IN_USE] = "in use: %s",
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A bottom)", [UI_MENU_PAD_LAYOUT_XBOX_PROMPTS] = "Xbox prompts (A right)",
[UI_MENU_PAD_LAYOUT_IN_USE] = "in use: %s",
[UI_MENU_PAD_LAYOUT_PROMPTS_MISSING] = "prompt files missing: run the setup again",
[UI_MENU_PAD_LAYOUT_PROMPTS_RESTART] = "prompts change after restart",
[UI_MENU_STICK_DEADZONE] = "Stick dead zone",
[UI_MENU_STICK_DEADZONE_DESC] = "How far a gamepad stick moves before the game sees it. Raise it if the character or the camera moves on its own.",
[UI_MENU_MOUSE_SENS] = "Mouse camera sensitivity",
[UI_MENU_MOUSE_SENS_DESC] = "How fast the mouse turns the camera.",
@@ -142,6 +155,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_PAD_L] = "Left bumper", [UI_PAD_R] = "Right bumper", [UI_PAD_START] = "Start", [UI_PAD_BACK] = "Back",
[UI_PAD_DPAD_LEFT] = "D-pad left", [UI_PAD_DPAD_UP] = "D-pad up", [UI_PAD_DPAD_RIGHT] = "D-pad right",
[UI_PAD_DPAD_DOWN] = "D-pad down", [UI_PAD_ZL] = "Left trigger", [UI_PAD_ZR] = "Right trigger",
[UI_PAD_LPADDLE1] = "Left back button", [UI_PAD_RPADDLE1] = "Right back button",
[UI_PAD_LPADDLE2] = "Left back button 2", [UI_PAD_RPADDLE2] = "Right back button 2",
[UI_PAD_CROSS] = "Cross", [UI_PAD_CIRCLE] = "Circle", [UI_PAD_SQUARE] = "Square", [UI_PAD_TRIANGLE] = "Triangle",
[UI_MENU_MENU_LANG] = "Menu language", [UI_MENU_MENU_LANG_DESC] = "Language of this menu and the overlay.",
@@ -185,6 +200,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_MEMORY_VERIFY] = "Verify",
[UI_MENU_TEX_ALIAS] = "Texture aliasing", [UI_MENU_TEX_ALIAS_DESC] = "A new texture created over a live texture's memory starts with that content instead of empty.",
[UI_MENU_TEX_VRAM_PRESSURE] = "Texture memory limit", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Percent of video memory above which textures the game released are dropped (they come back from storage when used again). 0 keeps them.",
[UI_MENU_ASTC_BC] = "Compress ASTC to BC3", [UI_MENU_ASTC_BC_DESC] = "With the GPU decoder: re-encodes decoded ASTC textures to BC3, a quarter of the video memory. For GPUs with 4 GB or less; textures look slightly blockier.",
[UI_MENU_LOD_GUARD] = "LOD guard", [UI_MENU_LOD_GUARD_DESC] = "Prevents objects flashing in the wrong place or a flat far field for a frame, by waiting for the game to finish rewriting level-of-detail data before the GPU reads it.",
[UI_MENU_FIX_C0] = "Constant buffer fix",
[UI_MENU_FIX_C0_DESC] = "Fills the driver-reserved constant bank c0 (storage sizes, dispatch size, point size, base instance) instead of reading zeros.",
@@ -271,7 +287,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_GRAPHICS] = "Изображение",
[UI_MENU_OUTPUT_RES] = "Разрешение игры",
[UI_MENU_OUTPUT_RES_DESC] = "Размер, в котором игра рисует картинку и интерфейс. Выше 1920 × 1080 игре нужно больше "
"видеопамяти и времени GPU.",
"видеопамяти и времени GPU. Более широкий размер (21:9) расширяет обзор камеры и "
"переносит интерфейс в углы экрана.",
[UI_MENU_RENDER_SCALE] = "Масштаб 3D-отрисовки",
[UI_MENU_RENDER_SCALE_DESC] = "Размер 3D-сцены относительно разрешения игры; FSR игры доводит её до этого размера. "
"Выше 100 % сглаживает лесенки на краях (суперсэмплинг) ценой большой нагрузки на GPU.",
@@ -280,6 +297,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_UPSCALER_DESC] = "Как картинка растягивается, когда окно больше разрешения игры. FSR сохраняет края и "
"детали чётче билинейного.",
[UI_MENU_UPSCALER_FSR] = "AMD FSR 1", [UI_MENU_UPSCALER_BILINEAR] = "Билинейное",
[UI_MENU_ASPECT] = "Форма картинки",
[UI_MENU_ASPECT_DESC] = "Когда форма окна отличается от разрешения игры (картинка 16:9 на экране 21:9): сохранить "
"форму картинки с чёрными полосами или растянуть её на всё окно.",
[UI_MENU_ASPECT_FIT] = "Чёрные полосы", [UI_MENU_ASPECT_STRETCH] = "Растянуть",
[UI_MENU_SHARPNESS] = "Резкость FSR",
[UI_MENU_SHARPNESS_DESC] = "Повышение резкости после масштабирования FSR; 0 выключает его.",
@@ -312,9 +333,18 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_PAD_LAYOUT] = "Раскладка кнопок",
[UI_MENU_PAD_LAYOUT_DESC] = "Какая кнопка работает как A. Nintendo: правая, как на Switch. Xbox: нижняя, как "
"подписано на контроллере Xbox (крест на контроллере PlayStation). Автоматически: "
"Nintendo для контроллеров с подписями Nintendo, Xbox для остальных.",
"Nintendo для контроллеров с подписями Nintendo, Xbox для остальных. Подсказки Xbox: "
"A справа, как в Nintendo, а игра показывает надписи Xbox (B вместо A, A вместо B, Y "
"вместо X, X вместо Y); подсказки меняются после перезапуска. Буквы записаны в файлах "
"игры, поэтому у кнопки, переназначенной на вкладке управления, на экране остаётся "
"прежняя буква.",
[UI_MENU_PAD_LAYOUT_AUTO] = "Автоматически", [UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo (A справа)",
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A снизу)", [UI_MENU_PAD_LAYOUT_IN_USE] = "сейчас: %s",
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A снизу)", [UI_MENU_PAD_LAYOUT_XBOX_PROMPTS] = "Подсказки Xbox (A справа)",
[UI_MENU_PAD_LAYOUT_IN_USE] = "сейчас: %s",
[UI_MENU_PAD_LAYOUT_PROMPTS_MISSING] = "нет файлов подсказок: запустите установку снова",
[UI_MENU_PAD_LAYOUT_PROMPTS_RESTART] = "подсказки сменятся после перезапуска",
[UI_MENU_STICK_DEADZONE] = "Мёртвая зона стиков",
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно отклонить стик геймпада, чтобы игра это заметила. Увеличьте, если персонаж или камера двигаются сами.",
[UI_MENU_MOUSE_SENS] = "Чувствительность мыши",
[UI_MENU_MOUSE_SENS_DESC] = "Как быстро мышь поворачивает камеру.",
@@ -364,6 +394,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_PAD_DPAD_LEFT] = "Крестовина влево", [UI_PAD_DPAD_UP] = "Крестовина вверх",
[UI_PAD_DPAD_RIGHT] = "Крестовина вправо", [UI_PAD_DPAD_DOWN] = "Крестовина вниз", [UI_PAD_ZL] = "Левый курок",
[UI_PAD_ZR] = "Правый курок",
[UI_PAD_LPADDLE1] = "Левая задняя кнопка", [UI_PAD_RPADDLE1] = "Правая задняя кнопка",
[UI_PAD_LPADDLE2] = "Левая задняя кнопка 2", [UI_PAD_RPADDLE2] = "Правая задняя кнопка 2",
[UI_PAD_CROSS] = "Крест", [UI_PAD_CIRCLE] = "Круг", [UI_PAD_SQUARE] = "Квадрат", [UI_PAD_TRIANGLE] = "Треугольник",
[UI_MENU_MENU_LANG] = "Язык меню", [UI_MENU_MENU_LANG_DESC] = "Язык этого меню и индикатора.",
@@ -407,6 +439,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_MEMORY_VERIFY] = "Проверка",
[UI_MENU_TEX_ALIAS] = "Наследование содержимого текстур", [UI_MENU_TEX_ALIAS_DESC] = "Новая текстура поверх памяти живой текстуры получает её содержимое, а не пустую.",
[UI_MENU_TEX_VRAM_PRESSURE] = "Предел памяти текстур", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Процент видеопамяти, выше которого освобождённые игрой текстуры удаляются (при повторном использовании загружаются снова). 0 — не удалять.",
[UI_MENU_ASTC_BC] = "Сжатие ASTC в BC3", [UI_MENU_ASTC_BC_DESC] = "С декодером на GPU: декодированные текстуры ASTC пересжимаются в BC3 и занимают вчетверо меньше видеопамяти. Для видеокарт с 4 ГБ и меньше; текстуры чуть более блочные.",
[UI_MENU_LOD_GUARD] = "Защита LOD", [UI_MENU_LOD_GUARD_DESC] = "Не даёт объектам мелькать не на своём месте, а дальнему плану — на кадр становиться плоским: ждёт, пока игра допишет данные уровней детализации, и только потом их читает GPU.",
[UI_MENU_FIX_C0] = "Исправление константного буфера",
[UI_MENU_FIX_C0_DESC] = "Заполняет зарезервированный драйвером банк констант c0 (размеры хранилищ, размер запуска, размер точки, базовый экземпляр) вместо чтения нулей.",
+7 -3
View File
@@ -35,6 +35,7 @@ typedef enum {
UI_MENU_OUTPUT_RES, UI_MENU_OUTPUT_RES_DESC,
UI_MENU_RENDER_SCALE, UI_MENU_RENDER_SCALE_DESC, UI_MENU_RENDER_SCALE_GAME,
UI_MENU_UPSCALER, UI_MENU_UPSCALER_DESC, UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR,
UI_MENU_ASPECT, UI_MENU_ASPECT_DESC, UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH,
UI_MENU_SHARPNESS, UI_MENU_SHARPNESS_DESC,
// Game
@@ -56,7 +57,9 @@ typedef enum {
UI_MENU_RUMBLE_CURVE, UI_MENU_RUMBLE_CURVE_DESC,
UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC,
UI_MENU_PAD_LAYOUT, UI_MENU_PAD_LAYOUT_DESC, UI_MENU_PAD_LAYOUT_AUTO, UI_MENU_PAD_LAYOUT_NINTENDO,
UI_MENU_PAD_LAYOUT_XBOX, UI_MENU_PAD_LAYOUT_IN_USE,
UI_MENU_PAD_LAYOUT_XBOX, UI_MENU_PAD_LAYOUT_XBOX_PROMPTS, UI_MENU_PAD_LAYOUT_IN_USE,
UI_MENU_PAD_LAYOUT_PROMPTS_MISSING, UI_MENU_PAD_LAYOUT_PROMPTS_RESTART,
UI_MENU_STICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
UI_MENU_MOUSE_SENS, UI_MENU_MOUSE_SENS_DESC,
// Controls tab: the remapping rows (ui/binds_page.cpp)
@@ -83,10 +86,10 @@ typedef enum {
UI_KEY_ENTER, UI_KEY_BACKSPACE, UI_KEY_RSHIFT, UI_KEY_RCTRL, UI_KEY_LALT, UI_KEY_CAPSLOCK, UI_KEY_INSERT,
UI_KEY_DELETE, UI_KEY_HOME, UI_KEY_END, UI_KEY_PAGEUP, UI_KEY_PAGEDOWN,
UI_KEY_NUM_FMT, // "Num %s": a numpad key
// gamepad buttons, in pad id order PAD_SOUTH .. PAD_ZR, then the PlayStation face labels
// gamepad buttons, in pad id order PAD_SOUTH .. PAD_RPADDLE2, then the PlayStation face labels
UI_PAD_SOUTH, UI_PAD_EAST, UI_PAD_WEST, UI_PAD_NORTH, UI_PAD_LSTICK, UI_PAD_RSTICK, UI_PAD_L, UI_PAD_R,
UI_PAD_START, UI_PAD_BACK, UI_PAD_DPAD_LEFT, UI_PAD_DPAD_UP, UI_PAD_DPAD_RIGHT, UI_PAD_DPAD_DOWN, UI_PAD_ZL,
UI_PAD_ZR,
UI_PAD_ZR, UI_PAD_LPADDLE1, UI_PAD_RPADDLE1, UI_PAD_LPADDLE2, UI_PAD_RPADDLE2,
UI_PAD_CROSS, UI_PAD_CIRCLE, UI_PAD_SQUARE, UI_PAD_TRIANGLE,
// Menu tab
@@ -119,6 +122,7 @@ typedef enum {
UI_MENU_MEMORY_VERIFY,
UI_MENU_TEX_ALIAS, UI_MENU_TEX_ALIAS_DESC,
UI_MENU_TEX_VRAM_PRESSURE, UI_MENU_TEX_VRAM_PRESSURE_DESC,
UI_MENU_ASTC_BC, UI_MENU_ASTC_BC_DESC,
UI_MENU_LOD_GUARD, UI_MENU_LOD_GUARD_DESC,
UI_MENU_FIX_C0, UI_MENU_FIX_C0_DESC,
UI_MENU_FIX_ZERO, UI_MENU_FIX_ZERO_DESC,
+7 -5
View File
@@ -100,11 +100,11 @@ void vk_present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, in
// vk_present_image split in two: vk_present_blit submits the blit and leaves the present waiting, vk_present_flip
// queues it. The next blit or a swapchain rebuild queues a waiting present first.
void vk_present_blit(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4]);
// FSR 1 on the present (vk_fsr.c): records EASU (+ RCAS) of src's crop rectangle {x, y, w, h} into dst, a dw x dh
// swapchain image in TRANSFER_DST; src is in GENERAL and fmt its format. 0 = nothing recorded (the setting says
// bilinear, no upscale is needed, or FSR is not available): the caller blits.
// FSR 1 on the present (vk_fsr.c): records EASU (+ RCAS) of src's crop rectangle {x, y, w, h} into the rectangle out
// {x, y, w, h} of dst, a swapchain image in TRANSFER_DST; src is in GENERAL and fmt its format. 0 = nothing recorded
// (the setting says bilinear, no upscale is needed, or FSR is not available): the caller blits.
int vk_fsr_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, int sw, int sh, const int crop[4],
VkImage dst, uint32_t dw, uint32_t dh);
VkImage dst, const int out[4]);
void vk_fsr_release(void); // before the device goes away
void vk_present_flip(void);
// Every submission to ctx->gfx_queue (and xfer_queue when shared) takes this lock; vk_present_image and the swapchain
@@ -242,6 +242,7 @@ enum { VK_ASTC_CPU, VK_ASTC_GPU, VK_ASTC_NATIVE, VK_ASTC_CHECK };
int vk_astc_mode(void); // decided once per device (logged); VK_ASTC_CPU before vk_init
int vk_astc_native(VkFormat astc_fmt); // 1: images of this ASTC format are created as such and get the raw blocks
int vk_astc_gpu(void); // 1: ASTC images decoded to RGBA8 by the compute decoder (vk_astc_record)
int vk_astc_bc(void); // 1: those images are BC3 instead, encoded after the decode (TOTK_ASTC_BC=1)
// One image region decoded from raw blocks: tight rows of 16-byte blocks at byte src of the source buffer (slices one
// after the other), w x h texels and slices deep, written at offset of level / layer.
typedef struct { VkDeviceSize src; uint32_t w, h, slices, level, layer; VkOffset3D offset; } VkAstcRegion;
@@ -249,9 +250,10 @@ typedef struct { VkDeviceSize src; uint32_t w, h, slices, level, layer; VkOffset
// bytes of blocks) goes to a device-local scratch buffer, a dispatch per region decodes it, and the texels are copied
// into img (VK_IMAGE_LAYOUT_GENERAL). The caller records its usual barriers around an image copy before and after the
// call. readback (optional, host-visible, TRANSFER_DST): also gets the decoded texels, the regions one after the other.
// bc: img is BC3 (TexVkFormat.astc_bc); the texels are encoded to BC3 blocks before the copy, and readback is ignored.
// 0 when the decoder is unavailable or the scratch buffer cannot be allocated (nothing recorded).
int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, const VkAstcRegion *rg, int n, uint32_t bw,
uint32_t bh, int srgb, VkImage img, VkImageAspectFlags aspect, VkBuffer readback);
uint32_t bh, int srgb, int bc, VkImage img, VkImageAspectFlags aspect, VkBuffer readback);
void vk_astc_stats(uint64_t *dispatches, uint64_t *texels); // since start
// TOTK_ASTC_TIME=1: GPU timestamps around each vk_astc_record's dispatches (logged per footprint and the largest one)
void vk_astc_time_enable(int on);
+100 -11
View File
@@ -1,6 +1,10 @@
// ASTC textures on the GPU: the TOTK_ASTC knob, a compute decoder that turns raw blocks into the RGBA8 image the CPU
// decoder (tex_astc.c) would have written, and the native ASTC formats where the device samples them itself.
//
// TOTK_ASTC_BC = 1 (gpu mode only, default 0): a second pass (runtime/shaders/bc3_encode.comp) encodes the decoded
// texels to BC3 and the image is BC3, 8 bits per texel instead of 32. For GPUs with little video memory, at the cost of
// BC3's quality: textures look slightly blockier, and data textures (normal maps) lose some precision.
//
// TOTK_ASTC = auto (default): native when the device reports textureCompressionASTC_LDR and the format's features,
// else gpu. gpu: compute decode into RGBA8 (runtime/shaders/astc_decode.comp). native: like auto, but logs when the
// device cannot. cpu: astc_decode_ldr_mt on the uploading thread, as before. check: gpu, and every upload is also
@@ -17,6 +21,7 @@
#include <windows.h>
#include "astc_decode_spv.h"
#include "bc3_encode_spv.h"
#include "cfg.h"
#include "host.h"
#include "tex.h"
@@ -99,6 +104,8 @@ static struct {
VkDescriptorSetLayout dsl;
VkPipelineLayout layout;
VkPipeline pipe;
VkPipeline bc_pipe; // the BC3 encoder (TOTK_ASTC_BC): the decoder's layout, bindings 0 and 1
int bc; // 1: ASTC images are BC3 (vk_astc_bc)
PFN_vkCmdPushDescriptorSetKHR push;
VkBuffer tab; // device-local copy of the tables
VmaAllocation tab_alloc;
@@ -293,6 +300,36 @@ static int create_pipeline(VkCtx *c) {
return 1;
}
// TOTK_ASTC_BC=1: the BC3 encoder, when the device samples BC3 (every desktop GPU does); 0 leaves ASTC as RGBA8.
static int create_bc_pipeline(VkCtx *c) {
const char *e = cfg_get("TOTK_ASTC_BC");
if (!e || !*e || *e == '0') return 0;
VkFormatProperties p;
vkGetPhysicalDeviceFormatProperties(c->phys, VK_FORMAT_BC3_SRGB_BLOCK, &p);
VkFormatFeatureFlags need = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
if ((p.optimalTilingFeatures & need) != need) {
rt_log("vk: TOTK_ASTC_BC: the device does not sample BC3; ASTC stays RGBA8");
return 0;
}
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof bc3_encode_spv,
.pCode = bc3_encode_spv};
VkShaderModule m = VK_NULL_HANDLE;
if (vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS) return 0;
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
.layout = G.layout};
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &G.bc_pipe);
vkDestroyShaderModule(c->device, m, NULL);
if (r != VK_SUCCESS) {
G.bc_pipe = VK_NULL_HANDLE;
rt_log("vk: TOTK_ASTC_BC: the BC3 encoder pipeline failed (%d); ASTC stays RGBA8", (int)r);
return 0;
}
rt_log("vk: ASTC textures are encoded to BC3 after the decode (TOTK_ASTC_BC)");
return 1;
}
static int native_device(VkCtx *c) { return c->has_astc_ldr; }
static const char *mode_name(int m) {
@@ -324,6 +361,7 @@ int vk_astc_mode(void) {
rt_log("vk: ASTC compute decoder unavailable on %s: ASTC decodes on the CPU", c->props.deviceName);
}
if (G.failed && m != VK_ASTC_NATIVE) m = VK_ASTC_CPU;
G.bc = m == VK_ASTC_GPU && G.pipe && create_bc_pipeline(c);
G.mode = m;
rt_log("vk: ASTC textures: %s (textureCompressionASTC_LDR %d)", mode_name(m), native_device(c));
}
@@ -352,6 +390,15 @@ int vk_astc_gpu(void) {
return ok;
}
int vk_astc_bc(void) {
VkCtx *c = vk_ctx();
if (!c || vk_astc_mode() != VK_ASTC_GPU) return 0;
AcquireSRWLockShared(&g_lock);
int on = G.dev == c->device && G.bc;
ReleaseSRWLockShared(&g_lock);
return on;
}
static void mem_barrier(VkCommandBuffer cb, VkPipelineStageFlags2 src, VkAccessFlags2 sa, VkPipelineStageFlags2 dst,
VkAccessFlags2 da) {
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = src, .srcAccessMask = sa,
@@ -368,30 +415,47 @@ static VkDeviceSize region_out(const VkAstcRegion *r) { return (VkDeviceSize)r->
static VkDeviceSize region_in(const VkAstcRegion *r, uint32_t bw, uint32_t bh) {
return (VkDeviceSize)((r->w + bw - 1) / bw) * ((r->h + bh - 1) / bh) * r->slices * 16;
}
static VkDeviceSize region_bc(const VkAstcRegion *r) { return region_in(r, 4, 4); } // BC3: 16 bytes per 4x4 block
typedef struct { uint32_t in_base, out_base, width, height, bx, bslice; } BcPush; // bc3_encode.comp Params
int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, const VkAstcRegion *rg, int n, uint32_t bw,
uint32_t bh, int srgb, VkImage img, VkImageAspectFlags aspect, VkBuffer readback) {
static volatile long n_fail;
uint32_t bh, int srgb, int bc, VkImage img, VkImageAspectFlags aspect, VkBuffer readback) {
static volatile long n_fail, n_unaligned;
(void)src_size;
VkCtx *c = vk_ctx();
if (!c || !vk_astc_gpu() || bw < 4 || bh < 4 || bw > 12 || bh > 12) return 0;
AcquireSRWLockShared(&g_lock);
VkPipeline pipe = G.dev == c->device ? G.pipe : VK_NULL_HANDLE;
VkPipeline pipe = G.dev == c->device ? G.pipe : VK_NULL_HANDLE, bc_pipe = G.dev == c->device ? G.bc_pipe : VK_NULL_HANDLE;
ReleaseSRWLockShared(&g_lock);
if (!pipe) return 0;
// chunks of consecutive regions; scratch = the largest chunk's blocks, then its texels
VkDeviceSize out_total = 0, cap_in = 0, cap_out = 0, ci = 0, co = 0;
if (!pipe || (bc && !bc_pipe)) return 0;
if (bc) {
readback = VK_NULL_HANDLE;
for (int i = 0; i < n; i++) // a BC copy starts on a block corner; a WriteTexels region of a 5x5 or 6x6 footprint may not
if ((rg[i].offset.x & 3) || (rg[i].offset.y & 3)) {
if (InterlockedIncrement(&n_unaligned) <= 16)
rt_log("vk: ASTC to BC3: region at %d,%d is not on a 4x4 block corner: texture left as it was",
rg[i].offset.x, rg[i].offset.y);
return 0;
}
}
// chunks of consecutive regions; scratch = the largest chunk's blocks, then its texels, then (bc) its BC3 blocks
VkDeviceSize out_total = 0, cap_in = 0, cap_out = 0, cap_bc = 0, ci = 0, co = 0, cb3 = 0;
for (int i = 0; i < n; i++) {
VkDeviceSize o = region_out(&rg[i]), b = region_in(&rg[i], bw, bh);
if (co && co + o > CHUNK_OUT) ci = co = 0;
ci += b, co += o, out_total += o;
if (co && co + o > CHUNK_OUT) ci = co = cb3 = 0;
ci += b, co += o, cb3 += region_bc(&rg[i]), out_total += o;
if (ci > cap_in) cap_in = ci;
if (co > cap_out) cap_out = co;
if (cb3 > cap_bc) cap_bc = cb3;
}
if (!out_total) return 1;
VkDeviceSize al = c->props.limits.minStorageBufferOffsetAlignment; // the texels bind at cap_in
if (al > 1) cap_in = (cap_in + al - 1) / al * al;
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = cap_in + cap_out,
VkDeviceSize al = c->props.limits.minStorageBufferOffsetAlignment; // the texels bind at cap_in, the BC3 blocks after them
if (al < 16) al = 16;
cap_in = (cap_in + al - 1) / al * al;
cap_out = (cap_out + al - 1) / al * al;
if (!bc) cap_bc = 0;
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = cap_in + cap_out + cap_bc,
.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT |
VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo aci = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
@@ -485,6 +549,30 @@ int vk_astc_record(VkCommandBuffer cb, VkBuffer src, VkDeviceSize src_size, cons
}
if (slot >= 0)
vkCmdWriteTimestamp2(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, tpool, 2 * (uint32_t)slot + 1);
if (bc) { // the chunk's texels to BC3 blocks behind them; the image copy then takes the blocks
mem_barrier(cb, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_READ_BIT);
VkDescriptorBufferInfo bdi[2] = {{sb, cap_in, cap_out}, {sb, cap_in + cap_out, cap_bc}};
VkWriteDescriptorSet bw2[2];
for (int k = 0; k < 2; k++)
bw2[k] = (VkWriteDescriptorSet){.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = (uint32_t)k,
.descriptorCount = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.pBufferInfo = &bdi[k]};
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, bc_pipe);
rec_push(G.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, G.layout, 0, 2, bw2);
VkDeviceSize to = 0, bo = 0;
for (int i = i0; i < i1; i++) {
const VkAstcRegion *r = &rg[i];
uint32_t bx4 = (r->w + 3) / 4, by4 = (r->h + 3) / 4;
BcPush pc = {.in_base = (uint32_t)(to / 4), .out_base = (uint32_t)(bo / 16), .width = r->w, .height = r->h,
.bx = bx4, .bslice = bx4 * by4};
vkCmdPushConstants(cb, G.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
vkCmdDispatch(cb, (bx4 + 7) / 8, (by4 + 7) / 8, r->slices);
VkBufferImageCopy *cp = &copies[i - i0];
cp->bufferOffset = cap_in + cap_out + bo, cp->bufferRowLength = bx4 * 4, cp->bufferImageHeight = by4 * 4;
to += region_out(r), bo += region_bc(r);
}
}
mem_barrier(cb, VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT,
VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
vkCmdCopyBufferToImage(cb, sb, img, VK_IMAGE_LAYOUT_GENERAL, (uint32_t)(i1 - i0), copies);
@@ -525,6 +613,7 @@ void vk_astc_release(void) {
AcquireSRWLockExclusive(&g_lock);
if (c && G.dev == c->device) {
if (G.pipe) vkDestroyPipeline(c->device, G.pipe, NULL);
if (G.bc_pipe) vkDestroyPipeline(c->device, G.bc_pipe, NULL);
if (G.layout) vkDestroyPipelineLayout(c->device, G.layout, NULL);
if (G.dsl) vkDestroyDescriptorSetLayout(c->device, G.dsl, NULL);
if (G.tab) vmaDestroyBuffer(c->vma, G.tab, G.tab_alloc);
+156
View File
@@ -23,6 +23,7 @@
#include "host.h"
#include "nvn_api.h"
#include "nvn_tex.h"
#include "platform/platform.h"
#include "tex.h"
#include "vk.h"
#include "vk_exec.h"
@@ -141,11 +142,163 @@ static int view_format(const tex_t *t, const view_t *v, int blocks, VkFormat *ou
return 1;
}
// ---- 3D BC render targets
// A 3D BC texture has no block views (vk_texture.c create_tex, TOTK_BLOCK_VIEWS_3D): on RDNA2 cards such an image
// made BLOCK_TEXEL_VIEW_COMPATIBLE and 2D_ARRAY_COMPATIBLE took GPU writes past the end of its memory (issue #5). Draws
// into it through an uncompressed view of its blocks (agl's BC4 encoder fills 32x32x32 and 64x64x64 volumes and their
// levels at the logo) go to a stand-in: a 2D array of the blocks of one level in the view's format, one layer per
// depth plane. It starts with the level's content, so a pass that writes part of a plane keeps the rest, and each pass
// end copies the planes it drew into the level (a copy between a compressed and a size-compatible uncompressed format).
typedef struct { VkImage bc, img; VmaAllocation alloc; VkFormat fmt; uint32_t level, w, h, d; } BcStandIn;
#define BC_STANDINS 64 // one per level drawn: 6 volumes of up to 7 levels at the logo
static BcStandIn bcs[BC_STANDINS];
static int nbcs;
static PlatMutex bcs_lock = PLAT_MUTEX_INIT; // the replay adds and reads; destroy_tex (any thread) removes
static void image_barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, uint32_t layers) {
VkImageMemoryBarrier2 ib = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = VK_PIPELINE_STAGE_2_NONE,
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
.oldLayout = from, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = img, .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, layers}};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1, .pImageMemoryBarriers = &ib};
vkCmdPipelineBarrier2(cb, &di);
}
static void memory_barrier(VkCommandBuffer cb, VkPipelineStageFlags2 src, VkAccessFlags2 sa, VkPipelineStageFlags2 dst,
VkAccessFlags2 da) {
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2, .srcStageMask = src, .srcAccessMask = sa,
.dstStageMask = dst, .dstAccessMask = da};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
vkCmdPipelineBarrier2(cb, &di);
}
// The stand-in of level level of a 3D BC image for draws through view format fmt, made on first use; 0 when none can
// be made (logged). Replay thread.
static int bc_standin(const VkTexImage *img, uint32_t level, VkFormat fmt, BcStandIn *out) {
static int warned;
plat_mutex_lock(&bcs_lock);
int full = nbcs == BC_STANDINS;
for (int i = 0; i < nbcs; i++)
if (bcs[i].bc == img->image && bcs[i].level == level && bcs[i].fmt == fmt) {
*out = bcs[i];
plat_mutex_unlock(&bcs_lock);
return 1;
}
plat_mutex_unlock(&bcs_lock);
if (full) {
if (!warned++) rt_log("vk: more than %d 3D BC render targets: the others are left out", BC_STANDINS);
return 0;
}
uint32_t lw = level_extent(img->width, level), lh = level_extent(img->height, level);
BcStandIn s = {img->image, VK_NULL_HANDLE, NULL, fmt, level, (lw + 3) / 4, (lh + 3) / 4,
level_extent(img->depth, level)};
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = fmt,
.extent = {s.w, s.h, 1}, .mipLevels = 1, .arrayLayers = s.d, .samples = VK_SAMPLE_COUNT_1_BIT,
.tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(vk_ctx()->vma, &ci, &ai, &s.img, &s.alloc, NULL) != VK_SUCCESS) {
if (!warned++) rt_log("vk: stand-in for a 3D BC render target could not be created: left out");
return 0;
}
// In the upload batch, which goes to the queue before this part: GENERAL, then the texture's blocks.
VkCommandBuffer cb = vk_upload_begin();
image_barrier(cb, s.img, VK_IMAGE_LAYOUT_UNDEFINED, s.d);
memory_barrier(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
VkImageCopy rg = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, 1},
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, s.d},
.extent = {lw, lh, s.d}}; // texels of the compressed source
vkCmdCopyImage(cb, img->image, VK_IMAGE_LAYOUT_GENERAL, s.img, VK_IMAGE_LAYOUT_GENERAL, 1, &rg);
memory_barrier(cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
vk_upload_end();
plat_mutex_lock(&bcs_lock);
bcs[nbcs++] = s;
plat_mutex_unlock(&bcs_lock);
rt_log("vk: 3D BC render target %ux%ux%u level %u drawn through a %ux%u x %u stand-in (format %d)", img->width,
img->height, img->depth, level, s.w, s.h, s.d, (int)fmt);
*out = s;
return 1;
}
// vk_exec_draw.c vk_exec_image_gone: the stand-in of a BC image that goes, freed once the GPU is done with it.
void vk_exec_bc_gone(VkImage image) {
plat_mutex_lock(&bcs_lock);
for (int i = 0; i < nbcs; i++)
if (bcs[i].bc == image) {
vk_defer_image(bcs[i].img, bcs[i].alloc);
bcs[i--] = bcs[--nbcs];
}
plat_mutex_unlock(&bcs_lock);
}
// At a pass end: the planes the pass drew into a stand-in go to its texture.
static void bc_copy_back(VkExec *ex, const VkExecTarget *t) {
plat_mutex_lock(&bcs_lock);
for (int i = 0; i < nbcs; i++) {
if (bcs[i].img != t->image) continue;
memory_barrier(ex->cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_COPY_BIT,
VK_ACCESS_2_TRANSFER_READ_BIT | VK_ACCESS_2_TRANSFER_WRITE_BIT);
VkImageCopy rg = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, t->base_layer, t->layers},
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, bcs[i].level, 0, 1},
.dstOffset = {0, 0, (int32_t)t->base_layer},
.extent = {bcs[i].w, bcs[i].h, t->layers}}; // texels of the uncompressed source
vkCmdCopyImage(ex->cb, t->image, VK_IMAGE_LAYOUT_GENERAL, bcs[i].bc, VK_IMAGE_LAYOUT_GENERAL, 1, &rg);
memory_barrier(ex->cb, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
ex->recorded++;
break;
}
plat_mutex_unlock(&bcs_lock);
}
// A draw into a 3D BC image without block views through an uncompressed view of its blocks: the target is the
// image's stand-in (bc_standin). 1 when o is filled (target or left out), 0 for any other target.
static int bc_target(const tex_t *t, const view_t *v, int has_v, const VkTexImage *img, VkExecTarget *o) {
int bw, bh;
if (img->type != VK_IMAGE_TYPE_3D || img->block_views || !nvn_fmt_bpb(t->d.format, &bw, &bh) || bw != 4 || bh != 4)
return 0;
VkFormat fmt;
BcStandIn s;
uint32_t level = has_v && v->set_levels ? v->base_level : 0;
if (level >= img->levels) {
warn_once(W_LEVEL, "render target level %u of a %u-level texture: left out", level, img->levels);
return 1;
}
if (!has_v || !v->set_format || !view_format(t, v, 1, &fmt) || !bc_standin(img, level, fmt, &s)) {
warn_once(W_BLOCK_VIEW, "render target format %#x (%ux%u) through view format %#x: no block view, left out",
t->d.format, t->d.width, t->d.height, has_v && v->set_format ? v->format : 0);
return 1;
}
uint32_t base = 0, layers = s.d;
if (v->set_layers) {
base = v->min_layer;
layers = v->nlayers ? v->nlayers : (base < s.d ? s.d - base : 0);
}
if (base >= s.d || !layers) return 1;
if (base + layers > s.d) layers = s.d - base;
o->image = s.img; // not img->image: view_of makes this command buffer's view of the stand-in
o->img = img;
o->format = fmt;
o->view_type = layers > 1 ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D;
o->aspect = VK_IMAGE_ASPECT_COLOR_BIT;
o->level = 0, o->base_layer = base, o->layers = layers;
o->width = s.w, o->height = s.h, o->samples = 1;
o->nvn_format = v->format;
return 1;
}
static void resolve_target(const tex_t *t, const view_t *v, int has_v, int depth_slot, VkExecTarget *o) {
memset(o, 0, sizeof *o);
o->tex = t;
if (!t) return;
const VkTexImage *img = vk_texture_get(t);
if (img && !depth_slot && bc_target(t, v, has_v, img, o)) return; // a 3D BC image: its stand-in
VkImageUsageFlags need = depth_slot ? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
int is3d = img && img->type == VK_IMAGE_TYPE_3D && !depth_slot; // slices drawn through 2D views (2D_ARRAY_COMPATIBLE)
if (!img || !(img->usage & need) || (img->type != VK_IMAGE_TYPE_2D && !is3d)) {
@@ -469,6 +622,9 @@ void vk_exec_pass_end(VkExec *ex) {
vkCmdEndRendering(ex->cb);
ex->recorded++;
ex->in_pass = 0;
for (int i = 0; i < ex->pass_ncolor && i < 8; i++) // stand-ins of 3D BC targets go to their textures
if (ex->color[i].image && ex->color[i].img && ex->color[i].img->image != ex->color[i].image)
bc_copy_back(ex, &ex->color[i]);
if (vk_dump_armed) vk_dump_pass_end(ex);
uint64_t stamp = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1;
for (int i = 0; i < 8; i++)
+2
View File
@@ -124,6 +124,8 @@ void vk_exec_inline_end(int reopen);
// buffers): the replay continues in a new one (vk_sync_split).
void vk_exec_draw_new_cb(void);
VkBuffer vk_exec_inline_buffer(VkDeviceSize size);
// vk_exec.c: the stand-in a 3D BC render target is drawn through goes with its image (vk_exec_image_gone).
void vk_exec_bc_gone(VkImage image);
// Scissor 0 clipped to the render area of the open pass; extent 0: nothing to draw.
VkRect2D vk_exec_scissor(const VkExec *ex);
void vk_exec_noatt_dims(const VkExec *ex, uint32_t *w, uint32_t *h); // pass size when no attachment is set
+1
View File
@@ -564,6 +564,7 @@ VkImageView vk_exec_target_view(const VkTexImage *img, VkFormat format, VkImageV
}
void vk_exec_image_gone(VkImage image) {
vk_exec_bc_gone(image);
if (!views_by_image.klen) return;
for (ViewNode *n = nvn_hm_del(&views_by_image, &image), *next; n; n = next) {
next = n->next;
+5
View File
@@ -126,6 +126,11 @@ int tex_vk_format(int nvn_format, TexVkFormat *o) {
o->astc_bh = astc[i].bh;
o->astc_srgb = (uint8_t)srgb;
o->note = "no native ASTC: decoded to RGBA8 (vk_astc.c compute decoder, or tex_astc.c with TOTK_ASTC=cpu)";
if (vk_astc_bc()) {
o->vk = srgb ? VK_FORMAT_BC3_SRGB_BLOCK : VK_FORMAT_BC3_UNORM_BLOCK;
o->astc_bc = 1;
o->note = "no native ASTC: decoded and encoded to BC3 on the GPU (vk_astc.c, TOTK_ASTC_BC)";
}
return 1;
}
if ((f >= 0x51 && f <= 0x68) || f == 0x6B || f == 0x6C) {
+6 -4
View File
@@ -4,8 +4,9 @@
// both apply at the next frame.
//
// Per frame: EASU reads the source through a view of its own format (the same colour conversion the blit does) and
// writes an RGBA16F image of the swapchain size; RCAS reads that and writes a second one; a 1:1 blit converts the
// result into the swapchain image, which the caller left in TRANSFER_DST. The shaders (runtime/shaders/fsr_*.comp)
// writes an RGBA16F image of the picture's size on the swapchain (all of it, or the part between black bars,
// gfx_window.c fit_rect); RCAS reads that and writes a second one; a 1:1 blit converts the result into its place in
// the swapchain image, which the caller left in TRANSFER_DST. The shaders (runtime/shaders/fsr_*.comp)
// compute AMD's constants from the push constants. Cost: two compute passes over the swapchain size and one copy per
// present; nothing on the game's draws.
//
@@ -156,8 +157,9 @@ static void bind_pass(VkCommandBuffer cb, VkPipeline p, VkImageView in, VkImageV
}
int vk_fsr_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, int sw, int sh, const int crop[4],
VkImage dst, uint32_t dw, uint32_t dh) {
VkImage dst, const int out[4]) {
int cw = crop[2], ch = crop[3];
uint32_t dw = (uint32_t)out[2], dh = (uint32_t)out[3];
if (strcmp(cfg_value("TOTK_UPSCALER"), "fsr") || (dw <= (uint32_t)cw && dh <= (uint32_t)ch) || !fmt) return 0;
VkCtx *c = vk_ctx();
if (!F.tried) init(c);
@@ -208,7 +210,7 @@ int vk_fsr_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt,
VkImageBlit region = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.srcOffsets = {{0, 0, 0}, {(int)dw, (int)dh, 1}},
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.dstOffsets = {{0, 0, 0}, {(int)dw, (int)dh, 1}}};
.dstOffsets = {{out[0], out[1], 0}, {out[0] + (int)dw, out[1] + (int)dh, 1}}};
vkCmdBlitImage(cb, F.img[last], VK_IMAGE_LAYOUT_GENERAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region,
VK_FILTER_NEAREST);
return 1;
+35 -2
View File
@@ -38,7 +38,7 @@ enum {
R_SetDepthWriteEnable, R_SetFrontFace, R_SetPrimitiveRestartEnable, R_SetPrimitiveTopology,
R_SetRasterizerDiscardEnable, R_SetScissorWithCount, R_SetStencilCompareMask, R_SetStencilOp, R_SetStencilReference,
R_SetStencilTestEnable, R_SetStencilWriteMask, R_SetViewportWithCount, R_WriteTimestamp, R_WriteTimestamp2,
R_PushTpl, R_Push, R_VertexInput, R_Bool, R_PolygonMode, R_BlendEnable, R_BlendEquation, R_WriteMask,
R_PushTpl, R_Push, R_VertexInput, R_Bool, R_PolygonMode, R_BlendEnable, R_BlendEquation, R_WriteMask, R_EndCb,
};
static uint64_t now_ns(void) {
@@ -131,7 +131,8 @@ void rec_begin(VkCommandBuffer cb) {
if (mode < 0) {
// Default off on AMD GPUs: with the recorder their drivers lost the device within the first frames or at menu
// opens, 1 boot in 3 on an RX 9070 XT, and TOTK_VK_RECORDER=0 ended it for three users (issue #5); under RADV
// the kernel saw an illegal opcode at the end of a command buffer (issue #40). The cause is not found.
// the kernel saw an illegal opcode at the end of a command buffer (issue #40). Letting the recorder end its own
// parts (rec_end_cb) fixed the white scene on an RX 6700 XT; the default stays off until more AMD cards run it.
const char *set = cfg_get("TOTK_VK_RECORDER");
mode = rt_knob("VK_RECORDER", vk_caps.amd ? "0" : "1");
if (!mode && vk_caps.amd && !(set && *set))
@@ -162,6 +163,32 @@ void rec_end(void) {
rec_live = 0;
}
// TOTK_REC_END (default 1): the thread that recorded the part also ends it, after the breadcrumb crumb (kind 0: none)
// that vk_sync.c writes at the end of a submission. On an RX 6700 XT (driver 26.10.1, issue #5) parts the recorder
// recorded and the submitting thread ended broke when a submission was split (vk_sync.c vk_sync_split): the GPU hung
// or the scene turned green, then white. The handoff in rec_drain orders the two threads, so Vulkan allows the old
// split; the driver did not cope with it. TOTK_REC_END=0 brings the old split back.
typedef struct { int kind; uint64_t n; } A_EndCb;
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n) {
static int mode = -1;
if (mode < 0) {
mode = rt_knob("REC_END", "1");
rt_log("vk_rec: a part the recorder records is ended by %s (TOTK_REC_END=%d)",
mode ? "the recorder" : "the submitting thread", mode);
}
if (!rec_live || cb != rec_cb || !mode) {
rec_end();
if (crumb) vk_crumb(cb, crumb, n);
return 0;
}
A_EndCb *a = put(R_EndCb, sizeof *a);
*a = (A_EndCb){crumb, n};
pub();
rec_drain();
rec_live = 0;
return 1;
}
void rec_stats_log(void) {
if (!(rt_trace & RT_TRACE_PERF) || !RS_.parts) return;
rt_log("vk_rec: %llu of %llu parts recorded by the recorder: %llu packets (%.1f MiB), %llu drains (%llu for a call "
@@ -588,6 +615,12 @@ static void exec_packet(uint32_t op, void *p) {
case R_BlendEnable: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEnableEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_BlendEquation: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEquationEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_WriteMask: { A_Targets *a = p; ((PFN_vkCmdSetColorWriteMaskEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_EndCb: {
A_EndCb *a = p;
if (a->kind) vk_crumb(cb, a->kind, a->n);
vkEndCommandBuffer(cb);
break;
}
default: rt_fatal("vk_rec: unknown packet %u", op);
}
}
+5 -1
View File
@@ -4,7 +4,8 @@
// ring, in call order, and the recorder makes the real call in that order; calls into any other command buffer (uploads,
// the window, probes) are made at once. A call the ring has no packet for (a structure with a pNext chain) waits for
// the recorder to empty the ring and is then made at once on the calling thread. rec_end returns once the recorder made
// every call, so ending, submitting and the command pool stay on the submitting thread.
// every call. vk_sync.c ends a part through rec_end_cb: the recorder makes the last calls into a part it recorded,
// vkEndCommandBuffer included; submitting and the command pool stay on the submitting thread.
//
// Every vkCmd* the runtime calls through vk.h goes through the rc_* wrappers below (the macros at the end); the
// extension commands called through function pointers have rec_* wrappers of their own. TOTK_VK_RECORDER=0 makes every
@@ -16,6 +17,9 @@
void rec_begin(VkCommandBuffer cb); // the submitting thread, sub_lock held
void rec_end(void); // waits for the recorder; no-op when rec_begin did not start it
// The end of part cb: the breadcrumb crumb (0: none, vk_crumbs.c), then vkEndCommandBuffer, both made by the recorder
// when it records cb, and the wait for it (1); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n);
void rec_drain(void); // waits until the recorder made every call queued so far
void rec_stats_log(void); // TOTK_TRACE=perf: packets, bytes, drains and waits since the last call
+3 -5
View File
@@ -293,9 +293,8 @@ static VkCommandBuffer prologue(VkCtx *c, uint64_t fence) {
VkCommandBuffer vk_sync_split(void) {
VkCtx *c = vk_ctx();
VkCommandBuffer cb = slots[cur_slot].cb;
rec_end(); // every command of the part is in cb
ts_end(cb, cur_fence);
vkEndCommandBuffer(cb);
if (!rec_end_cb(cb, 0, 0)) vkEndCommandBuffer(cb); // every command of the part is in cb, ended by its recorder
vk_upload_flush(0); // the uploads this part asked for precede it in queue order
VkCommandBuffer pro = prologue(c, cur_fence);
VkCommandBufferSubmitInfo cbi[2] = {{.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = pro},
@@ -348,11 +347,10 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
rec_begin(s->cb);
int recorded = vk_exec_record(s->cb, handles, n, fence);
s = &slots[cur_slot]; // the last part (vk_sync_split)
rec_end();
vk_crumb(s->cb, VK_CRUMB_SUBMIT_END, fence); // the recorder is done with the part
if (recorded) ts_end(s->cb, fence);
else ts_drop();
vkEndCommandBuffer(s->cb);
// The recorder is done with the part: the end-of-submission crumb, then the end, by the recorder when it records.
if (!rec_end_cb(s->cb, VK_CRUMB_SUBMIT_END, fence)) vkEndCommandBuffer(s->cb);
perf_replay(t0);
// After the recording: the uploads it asked for (and every earlier one) precede it in queue order.
vk_upload_flush(0);
+21 -4
View File
@@ -328,7 +328,18 @@ static Tex *create_tex(const tex_t *t, const TexKey *k, int defer_init) {
.samples = (VkSampleCountFlagBits)samples, .tiling = VK_IMAGE_TILING_OPTIMAL, .usage = usage,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
int bw, bh, block_views = 0;
if (!ds && samples == 1 && f.vk == f.ideal && nvn_fmt_bpb(d->format, &bw, &bh) && bw == 4 && bh == 4) {
// TOTK_BLOCK_VIEWS_3D (default 0): a 3D BC image also gets block views. On RDNA2 cards (driver 26.10.1, issue #5)
// the 32x32x32 BC4 volume the game creates at the logo, made BLOCK_TEXEL_VIEW_COMPATIBLE and 2D_ARRAY_COMPATIBLE,
// took a GPU write just past the end of its memory: device lost at the first submit after it when the image ended
// its memory block. Draws into a 3D BC image go through a stand-in instead (vk_exec.c bc_target).
static int bv3d = -1;
if (bv3d < 0) {
bv3d = rt_knob("BLOCK_VIEWS_3D", "0");
rt_log("vk: 3D BC textures %s (TOTK_BLOCK_VIEWS_3D=%d)", bv3d ? "get block views (renderable)" : "have no block views",
bv3d);
}
if (!ds && samples == 1 && f.vk == f.ideal && nvn_fmt_bpb(d->format, &bw, &bh) && bw == 4 && bh == 4 &&
(type != VK_IMAGE_TYPE_3D || bv3d)) {
VkImageCreateInfo bc = ci; // BC formats: renderable through views of their blocks (block_views_ok)
bc.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
bc.flags |= VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT;
@@ -646,7 +657,7 @@ static void upload(Tex *e) {
int checked = 0;
if (agpu) {
uint64_t a0 = perf_begin();
checked = vk_astc_record(cb, sb, total, ar, (int)n, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, e->pub.image,
checked = vk_astc_record(cb, sb, total, ar, (int)n, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, e->f.astc_bc, e->pub.image,
e->pub.aspect, rbuf);
perf_end(PERF_ASTC, a0);
if (!checked)
@@ -730,6 +741,7 @@ static void make_fresh_snap(Tex *e) {
// Bytes per block and block size of the image's VkFormat (ASTC decodes to RGBA8, RGB expands to RGBA); 0 when the
// VkFormat substitutes another one of unknown size.
static int img_block(const Tex *e, int *bw, int *bh) {
if (e->f.astc_bc) { *bw = *bh = 4; return 16; }
if (e->f.astc_bw) { *bw = *bh = 1; return 4; }
int b = nvn_fmt_bpb(e->t.d.format, bw, bh);
if (e->f.expand_rgb) { *bw = *bh = 1; return b / 3 * 4; }
@@ -1204,13 +1216,18 @@ static int image_texels(Tex *e, const Region *g, const NVNcopyRegion *r, uint8_t
vmaFlushAllocation(c->vma, sa, 0, VK_WHOLE_SIZE);
barrier(cb, ALL_RW, VK_PIPELINE_STAGE_2_COPY_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
int done = 0;
if (agpu) {
// A BC3 image takes whole 4x4 blocks, or a part that ends on the mip's edge; a region of a 5x5 or 6x6 footprint
// may end inside a block (vk_astc_record checks the start corner).
uint32_t mw = e->t.d.width >> g->level, mh = e->t.d.height >> g->level;
int bc_cut = e->f.astc_bc && (((w & 3) && (uint32_t)r->xoffset + w != (mw ? mw : 1)) ||
((h & 3) && (uint32_t)r->yoffset + h != (mh ? mh : 1)));
if (agpu && !bc_cut) { // bc_cut: nothing recorded, logged below as not decoded
VkAstcRegion *ar = calloc((size_t)g->nz, sizeof *ar);
for (int z = 0; z < g->nz; z++)
ar[z] = (VkAstcRegion){.src = slice * (size_t)z, .w = w, .h = h, .slices = 1, .level = rg[z].imageSubresource.mipLevel,
.layer = rg[z].imageSubresource.baseArrayLayer, .offset = rg[z].imageOffset};
uint64_t a0 = perf_begin();
done = vk_astc_record(cb, sb, slice * (size_t)g->nz, ar, g->nz, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb,
done = vk_astc_record(cb, sb, slice * (size_t)g->nz, ar, g->nz, e->f.astc_bw, e->f.astc_bh, e->f.astc_srgb, e->f.astc_bc,
e->pub.image, aspect, VK_NULL_HANDLE);
perf_end(PERF_ASTC, a0);
free(ar);
+51 -1
View File
@@ -14,7 +14,7 @@ from types import SimpleNamespace
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from installer import ini, runner, state # noqa: E402
from installer import ini, runner, state, steps # noqa: E402
from installer.events import Reporter # noqa: E402
from installer.layout import Layout # noqa: E402
@@ -241,6 +241,56 @@ class IniTest(unittest.TestCase):
self.assertEqual(ini.get_language(text), 'ja')
class CleanupTest(unittest.TestCase):
"""The real Cleanup step on a finished install whose romfs/ is intact, emptied or gone (#107)."""
def setUp(self):
self.steps = steps
self.tmp = tempfile.TemporaryDirectory()
base = Path(self.tmp.name)
self.dest, temp = base / 'dest', base / 'temp'
(self.dest / 'romfs').mkdir(parents=True)
temp.mkdir()
image = b'romfs image'
(self.dest / 'romfs' / 'romfs.bin').write_bytes(image)
digest = steps.extract.sha256(self.dest / 'romfs' / 'romfs.bin')
(self.dest / 'romfs' / 'index.json').write_text(json.dumps({'romfs_size': len(image), 'romfs_sha256': digest}))
self.expected = {'romfs': {'size': len(image), 'sha256': digest}}
self.ctx = SimpleNamespace(reporter=Reporter(out=io.StringIO(), log_path=base / 'x.log'), dest=self.dest,
temp=temp, lang='en-US', state={},
layout=SimpleNamespace(ini_example=ROOT / 'totk.ini.example'))
self.saved = steps.expected_update
steps.expected_update = lambda ctx: self.expected
def tearDown(self):
self.steps.expected_update = self.saved
self.ctx.reporter.close()
self.tmp.cleanup()
def run_cleanup(self):
try:
self.steps.Cleanup().run(self.ctx)
return 'ok'
except self.steps.InstallError as e:
return e.id
def test_intact_romfs_passes(self):
self.assertEqual(self.run_cleanup(), 'ok')
def test_emptied_romfs_fails(self):
for p in (self.dest / 'romfs').iterdir():
p.unlink()
self.assertEqual(self.run_cleanup(), 'cleanup.romfs_unreadable')
def test_missing_romfs_fails(self):
self.steps.remove(self.dest / 'romfs')
self.assertEqual(self.run_cleanup(), 'cleanup.romfs_unreadable')
def test_romfs_of_another_size_fails(self):
(self.dest / 'romfs' / 'romfs.bin').write_bytes(b'short')
self.assertEqual(self.run_cleanup(), 'cleanup.romfs_unreadable')
class EventsTest(unittest.TestCase):
def test_lines_are_json_and_the_log_is_english(self):
with tempfile.TemporaryDirectory() as d:
+111
View File
@@ -0,0 +1,111 @@
"""tools/pack_shader_cache.py on a fake shader cache: the pack layout gfx_shader.c reads (sorted index, bounds, data),
loose files folded in and deleted, a second run merging new loose files, a loose file winning over its packed copy,
malformed files skipped and --keep. No game files; stdlib only.
Run: python tests/pack_shader_cache_test.py
"""
import hashlib
import struct
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / 'tools'))
import pack_shader_cache as psc # noqa: E402
USE = struct.pack('<5I', 0x4d554b54, 1, 3, 0, 1) # gfx_shader.c use_store
def name(seed):
return hashlib.sha256(seed.encode()).hexdigest()
def read_pack(path):
"""{(kind, name hex): bytes}, checking what gfx_shader.c pack_open checks."""
b = path.read_bytes()
assert b[:8] == psc.MAGIC
count = struct.unpack_from('<I', b, 8)[0]
out, last = {}, None
for i in range(count):
nm, kind, size, offset = struct.unpack_from('<32sIIQ', b, 16 + 48 * i)
assert offset + size <= len(b) and offset >= 16 + 48 * count
assert last is None or (last[0], last[1]) < (kind, nm), 'index not sorted by (kind, name)'
last = (kind, nm)
out[(kind, nm.hex())] = b[offset:offset + size]
return out
class PackTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.cache = Path(self.tmp.name) / 'shader-cache'
(self.cache / 'in').mkdir(parents=True)
def tearDown(self):
self.tmp.cleanup()
def shader(self, seed, spv=b'\x03\x02\x23\x07spirv', res=b'{"x":1}'):
k = name(seed)
(self.cache / f'{k}.spv').write_bytes(spv + seed.encode())
(self.cache / f'{k}.res.json').write_bytes(res)
(self.cache / f'{name("use" + seed)}.use').write_bytes(USE)
(self.cache / 'in').mkdir(exist_ok=True) # in_store makes it again after a pack removed it
(self.cache / 'in' / f'{name("in" + seed)}.in').write_bytes(b'TKSI' + seed.encode())
return k
def test_pack_holds_every_file_and_deletes_the_loose_ones(self):
keys = [self.shader(str(i)) for i in range(50)]
(self.cache / 'notes.txt').write_text('kept')
(self.cache / f'{keys[0]}.code').write_bytes(b'kept too')
n, new, skipped, _ = psc.pack(self.cache)
self.assertEqual((n, new, skipped), (200, 200, 0))
got = read_pack(self.cache / psc.PACK)
self.assertEqual(got[(1, keys[3])], b'\x03\x02\x23\x07spirv3')
self.assertEqual(got[(2, keys[3])], b'{"x":1}')
self.assertEqual(got[(3, name('use3'))], USE)
self.assertEqual(got[(4, name('in3'))], b'TKSI3')
left = sorted(p.name for p in self.cache.iterdir())
self.assertEqual(left, sorted([psc.PACK, 'notes.txt', f'{keys[0]}.code'])) # in/ removed when empty
def test_second_run_merges_new_loose_files_and_loose_wins(self):
a = self.shader('a')
psc.pack(self.cache)
b = self.shader('b')
(self.cache / f'{a}.res.json').write_bytes(b'{"newer":1}')
n, new, _, _ = psc.pack(self.cache)
got = read_pack(self.cache / psc.PACK)
self.assertEqual(n, 8)
self.assertEqual(new, 5)
self.assertEqual(got[(2, a)], b'{"newer":1}')
self.assertEqual(got[(1, b)], b'\x03\x02\x23\x07spirvb')
def test_malformed_files_are_skipped_or_replaced_by_the_packed_copy(self):
a = self.shader('a')
psc.pack(self.cache)
(self.cache / f'{name("usea")}.use').write_bytes(b'short') # bad loose copy: the packed one stays
(self.cache / f'{name("bad")}.use').write_bytes(b'short') # bad and nowhere else: skipped
(self.cache / f'{name("empty")}.spv').write_bytes(b'')
n, _, skipped, _ = psc.pack(self.cache)
got = read_pack(self.cache / psc.PACK)
self.assertEqual(got[(3, name('usea'))], USE)
self.assertNotIn((3, name('bad')), got)
self.assertEqual((n, skipped), (4, 2))
self.assertTrue((self.cache / f'{name("bad")}.use').exists()) # never deleted unless packed
def test_keep_leaves_the_loose_files(self):
k = self.shader('a')
psc.pack(self.cache, keep=True)
self.assertTrue((self.cache / f'{k}.spv').exists())
self.assertEqual(len(read_pack(self.cache / psc.PACK)), 4)
def test_a_broken_pack_is_rebuilt_from_the_loose_files(self):
self.shader('a')
(self.cache / psc.PACK).write_bytes(b'garbage')
n, _, _, _ = psc.pack(self.cache)
self.assertEqual(n, 4)
if __name__ == '__main__':
unittest.main()
+12 -10
View File
@@ -174,16 +174,18 @@ def write_if_changed(path, text):
def copy_romfs_patches(version, out):
"""totk.exe loads romfs-patches beside romfs-index.bin (runtime/romfs.c overlay_init). tools/make_romfs_patches.py
writes them into build/<version> only, so a build in another directory ran without the 60 fps overlay (the quick
menus' captured background turned black). Copies them when their manifest differs."""
src = ROOT / 'build' / version / 'romfs-patches'
dst = out / 'romfs-patches'
if src.resolve() == dst.resolve() or not (src / 'manifest.tsv').is_file():
return
if (dst / 'manifest.tsv').is_file() and (dst / 'manifest.tsv').read_bytes() == (src / 'manifest.tsv').read_bytes():
return
shutil.rmtree(dst, ignore_errors=True)
shutil.copytree(src, dst)
print(f'romfs patches: copied from {src}')
menus' captured background turned black). Copies them when their manifest differs; romfs-patches-xbox (the same with
the A/B and X/Y prompts swapped, pad_layout = xbox_prompts) the same way when it was made."""
for name in ('romfs-patches', 'romfs-patches-xbox'):
src = ROOT / 'build' / version / name
dst = out / name
if src.resolve() == dst.resolve() or not (src / 'manifest.tsv').is_file():
continue
if (dst / 'manifest.tsv').is_file() and (dst / 'manifest.tsv').read_bytes() == (src / 'manifest.tsv').read_bytes():
continue
shutil.rmtree(dst, ignore_errors=True)
shutil.copytree(src, dst)
print(f'romfs patches: copied {name} from {src}')
def c_string(s):
+2 -1
View File
@@ -98,7 +98,8 @@ LLVM_SKIP_SUFFIXES = {'.idl', '.tlb'}
# tools/ files the installer steps run (and what they import), copied from the repo.
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'check_content.py', 'extract.py', 'hostmem.py', 'make_romfs_patches.py',
'prepare_shaders.py', 'romfs_index.py', 'shader_census.py', 'shader_list.py']
'pack_shader_cache.py', 'prepare_shaders.py', 'romfs_index.py', 'shader_census.py', 'shader_list.py',
'ultrawide_layout.py']
TOOLS_DIRS = ['vendor/macnativegames', 'data'] # data/: expected hashes
RUNTIME_SUFFIXES = {'.h', '.inc', '.ico'} # .ico: the installer window icon
+308 -19
View File
@@ -11,13 +11,33 @@ captured at once (upstream checked weapon, shield, material and ability wheel at
The game-over flow fix of the same upstream script is 1.0.0 only (1.2.1 and 1.4.3 run DmF_SY_GameOver.110) and is
left out.
--aspect <w>x<h> (a game resolution wider than 16:9, such as 3440x1440) also lays the UI out for that shape
(tools/ultrawide_layout.py): menus stay 16:9 in the middle, full-screen backgrounds cover the width and the HUD moves
to the screen's corners. totk.exe loads that overlay from romfs-patches-<w>x<h> when the game resolution is that size
(runtime/romfs.c overlay_init); the camera's side of it is the patch set aspect (recomp/patches-v143.txt).
--xbox-prompts: the same overlay with the A and B, and the X and Y button prompts swapped, for pad_layout = xbox_prompts
(runtime/romfs.c overlay_init loads romfs-patches-xbox in place of romfs-patches). That layout plays an Xbox pad by
position, as nintendo does: the button that does Switch A is labelled B, and the prompts then name the label. Built from
the game's own images and glyphs, nothing added:
- the action prompt (blyt/DoCommand_00.bflyt) draws the letter images Nt_KeyTexA/B/X/Y_00 of timg/__Combined.bntx in
the Common archive: their image data is swapped (same format, size and tiling). Around it, a diamond names the
other face buttons with one-letter text panes (T_B_00, T_X_00, ...). The game fills them from the messages of the
same name in LayoutMsg/DoCommand_00.msbt of each Mals/<language> archive, over the layout's own text: both have
their letters swapped the same way. The diamond's shape (Nt_KeyBtnSubBase*) shows the button's position, which
stays right.
- every other prompt is text: the button glyphs U+E040 A, U+E041 B, U+E042 X, U+E043 Y of scft/External.bfotf, the
same font in each Font/Font*.Nin_NX_NVN.bfarc.zs. Their CFF outlines and horizontal metrics are swapped.
Input: build/extracted/<version>/romfs (romfs.bin and index.json, verified by tools/extract.py). Output:
build/<version>/romfs-patches/<path> by default, and <out>/manifest.tsv (path, size, SHA-256 of the overlay, SHA-256 of the original). Run with .venv-py314.
build/<version>/romfs-patches/<path> by default (romfs-patches-xbox with --xbox-prompts), and <out>/manifest.tsv (path, size, SHA-256 of the overlay, SHA-256 of the original). Run with .venv-py314.
"""
import argparse
import compression.zstd as zstd
import hashlib
import importlib.util
import json
import re
import struct
from pathlib import Path
@@ -25,17 +45,20 @@ root = Path(__file__).resolve().parents[1]
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--version', choices=('v121', 'v143'), default='v121')
ap.add_argument('--romfs', type=Path, help='default build/extracted/<version>/romfs')
ap.add_argument('--out', type=Path, help='default build/<version>/romfs-patches')
ap.add_argument('--out', type=Path,
help='default build/<version>/romfs-patches (romfs-patches-xbox with --xbox-prompts)')
ap.add_argument('--aspect', help='<w>x<h> of a game resolution wider than 16:9: also lay the UI out for it')
ap.add_argument('--xbox-prompts', action='store_true', help='swap the A/B and X/Y button prompts as well')
args = ap.parse_args()
args.romfs = args.romfs or root / 'build/extracted' / args.version / 'romfs'
args.out = args.out or root / 'build' / args.version / 'romfs-patches'
args.out = args.out or root / 'build' / args.version / ('romfs-patches-xbox' if args.xbox_prompts else 'romfs-patches')
index = json.loads((args.romfs / 'index.json').read_text())
assert index['status'] == 'pass'
files = {f['path']: f for f in index['files']}
def sarc_members(data):
"""name -> (absolute start, end) of every SARC member."""
def sarc_nodes(data):
"""(name, node offset, absolute start, end) of every SARC member, in SFAT order."""
assert data[:4] == b'SARC' and data[6:8] == b'\xff\xfe'
header, size, start = struct.unpack_from('<HxxII', data, 4)
assert size == len(data) and data[header:header + 4] == b'SFAT'
@@ -43,14 +66,20 @@ def sarc_members(data):
names = header + 12 + count * 16
assert data[names:names + 4] == b'SFNT'
names += 8
out = {}
out = []
for i in range(count):
_, attribute, lo, hi = struct.unpack_from('<4I', data, header + 12 + i * 16)
node = header + 12 + i * 16
_, attribute, lo, hi = struct.unpack_from('<4I', data, node)
offset = names + (attribute & 0xffffff) * 4
out[data[offset:data.index(0, offset, start)].decode()] = (start + lo, start + hi)
out.append((data[offset:data.index(0, offset, start)].decode(), node, start + lo, start + hi))
return out
def sarc_members(data):
"""name -> (absolute start, end) of every SARC member."""
return {name: (lo, hi) for name, _, lo, hi in sarc_nodes(data)}
def flvi_keys(data, base):
"""(pane name, absolute key offset, keys) of every FLVI (visibility) step track in the BFLAN at data[base:]."""
assert data[base:base + 4] == b'FLAN' and data[base + 4:base + 6] == b'\xff\xfe'
@@ -77,12 +106,226 @@ def flvi_keys(data, base):
return out
def bntx_swap(data, base, pairs):
"""Swap the image data of each (name, name) pair of textures in the BNTX at data[base:] (names without the ^
suffix). Both of a pair must have the same header from flags to layout (format, size, tiling, mips) and image
size, so the bytes of one are a valid image of the other."""
assert data[base:base + 4] == b'BNTX' and data[base + 0x20:base + 0x24] == b'NX '
count, table = struct.unpack_from('<IQ', data, base + 0x24)
found = {}
for k in range(count):
info = base + struct.unpack_from('<Q', data, base + table + 8 * k)[0]
assert data[info:info + 4] == b'BRTI'
name = base + struct.unpack_from('<Q', data, info + 0x60)[0]
name = data[name + 2:name + 2 + struct.unpack_from('<H', data, name)[0]].decode().split('^')[0]
mips = base + struct.unpack_from('<Q', data, info + 0x70)[0]
shape = bytes(data[info + 0x10:info + 0x3c]) + bytes(data[info + 0x50:info + 0x54])
start, size = base + struct.unpack_from('<Q', data, mips)[0], struct.unpack_from('<I', data, info + 0x50)[0]
found[name] = (shape, start, size)
for a, b in pairs:
assert a in found and b in found, f'{a} or {b} missing from the texture list'
(shape_a, at_a, size), (shape_b, at_b, _) = found[a], found[b]
assert shape_a == shape_b, f'{a} and {b} differ in format, size or tiling'
data[at_a:at_a + size], data[at_b:at_b + size] = data[at_b:at_b + size], data[at_a:at_a + size]
def bflyt_letters(data, base, end, swap):
"""Swap the text of the one-letter text panes T_<letter>_<n> (A, B, X or Y) in the BFLYT at data[base:end] by
swap (letter -> letter); the names of the panes changed. Same length (one UTF-16 letter and its terminator)."""
assert data[base:base + 4] == b'FLYT'
pos, changed = base + struct.unpack_from('<H', data, base + 6)[0], []
while pos + 8 <= end:
magic, size = data[pos:pos + 4], struct.unpack_from('<I', data, pos + 4)[0]
if magic == b'txt1':
name = data[pos + 12:pos + 36].split(b'\0')[0].decode()
length = struct.unpack_from('<H', data, pos + 0x56)[0]
at = pos + struct.unpack_from('<I', data, pos + 0x64)[0]
letter = data[at:at + 2].decode('utf-16-le') if length == 4 and at > pos else ''
if letter in swap and re.fullmatch(r'T_[ABXY]_\d+', name):
data[at:at + 2] = swap[letter].encode('utf-16-le')
changed.append(name)
if not size:
break
pos += size
return changed
def msbt_letters(data, swap):
"""Swap the one-letter messages labelled T_<letter>_<n> (A, B, X or Y) in the UTF-16 MSBT data (a bytearray) by
swap; the labels changed. The game sets a layout's text panes from the messages of the same name in
Mals/<language>/LayoutMsg/<layout>.msbt, over the text stored in the layout."""
assert data[:8] == b'MsgStdBn' and data[0x0c] == 1, 'not a UTF-16 MSBT'
sections, pos = {}, 0x20
for _ in range(struct.unpack_from('<H', data, 0x0e)[0]):
size = struct.unpack_from('<I', data, pos + 4)[0]
sections[bytes(data[pos:pos + 4])] = (pos + 16, size)
pos = (pos + 16 + size + 15) & ~15
labels, (base, _) = {}, sections[b'LBL1']
for g in range(struct.unpack_from('<I', data, base)[0]):
count, at = struct.unpack_from('<II', data, base + 4 + 8 * g)
at += base
for _ in range(count):
length = data[at]
labels[struct.unpack_from('<I', data, at + 1 + length)[0]] = data[at + 1:at + 1 + length].decode()
at += 1 + length + 4
(base, size), changed = sections[b'TXT2'], []
count = struct.unpack_from('<I', data, base)[0]
offsets = list(struct.unpack_from(f'<{count}I', data, base + 4)) + [size]
for i in range(count):
at, length = base + offsets[i], offsets[i + 1] - offsets[i]
letter = data[at:at + 2].decode('utf-16-le') if length == 4 else ''
if letter in swap and re.fullmatch(r'T_[ABXY]_\d+', labels.get(i, '')):
data[at:at + 2] = swap[letter].encode('utf-16-le')
changed.append(labels[i])
return changed
def cff_index(data, pos):
"""([(start, end)] of every item, offSize, end) of the CFF INDEX at data[pos:]."""
count = struct.unpack_from('>H', data, pos)[0]
if not count:
return [], 0, pos + 2
size = data[pos + 2]
offsets = [int.from_bytes(data[pos + 3 + i * size:pos + 3 + (i + 1) * size], 'big') for i in range(count + 1)]
first = pos + 2 + (count + 1) * size
return [(first + offsets[i], first + offsets[i + 1]) for i in range(count)], size, first + offsets[-1]
def cff_charstrings(data, cff):
"""The offset (from the CFF table) of the CharStrings INDEX: operator 17 of the Top DICT. CID-keyed fonts (ROS,
operator 12 30) are refused: their glyphs can use different private dictionaries."""
_, _, pos = cff_index(data, cff + data[cff + 2]) # Name INDEX after the header
top, _, _ = cff_index(data, pos)
(pos, end), operands, found = top[0], [], None
while pos < end:
b = data[pos]
if b <= 21:
op = (12, data[pos + 1]) if b == 12 else b
pos += 2 if b == 12 else 1
assert op != (12, 30), 'CID-keyed CFF'
if op == 17:
found = operands[-1]
operands = []
elif b == 28:
operands.append(struct.unpack_from('>h', data, pos + 1)[0]); pos += 3
elif b == 29:
operands.append(struct.unpack_from('>i', data, pos + 1)[0]); pos += 5
elif b == 30: # real number: nibbles up to an 0xf
pos += 1
while data[pos] & 0x0f != 0x0f and data[pos] & 0xf0 != 0xf0:
pos += 1
pos += 1
operands.append(0)
elif b <= 246:
operands.append(b - 139); pos += 1
elif b <= 250:
operands.append((b - 247) * 256 + data[pos + 1] + 108); pos += 2
else:
operands.append(-(b - 251) * 256 - data[pos + 1] - 108); pos += 2
assert found is not None, 'no CharStrings in the Top DICT'
return found
def otf_glyph(data, cmap, code):
"""The glyph id of a code point in the format 4 (platform 3 encoding 1) subtable of the cmap at data[cmap:]."""
for k in range(struct.unpack_from('>H', data, cmap + 2)[0]):
platform, encoding, offset = struct.unpack_from('>HHI', data, cmap + 4 + 8 * k)
p = cmap + offset
if (platform, encoding) != (3, 1) or struct.unpack_from('>H', data, p)[0] != 4:
continue
seg = struct.unpack_from('>H', data, p + 6)[0] // 2
for s in range(seg):
end = struct.unpack_from('>H', data, p + 14 + 2 * s)[0]
start = struct.unpack_from('>H', data, p + 16 + 2 * seg + 2 * s)[0]
if not start <= code <= end:
continue
delta = struct.unpack_from('>h', data, p + 16 + 4 * seg + 2 * s)[0]
at = p + 16 + 6 * seg + 2 * s
ro = struct.unpack_from('>H', data, at)[0]
glyph = code if not ro else struct.unpack_from('>H', data, at + ro + 2 * (code - start))[0]
return (glyph + delta) & 0xffff if glyph else 0
raise AssertionError(f'U+{code:04X} is not in the font')
def otf_checksum(data):
data = bytes(data) + bytes(-len(data) % 4)
return sum(struct.unpack(f'>{len(data) // 4}I', data)) & 0xffffffff
def bfotf_swap(data, pairs):
"""The BFOTF data with the glyphs of each (code point, code point) pair swapped: CFF outlines and horizontal
metrics, then the table checksums. A BFOTF is an OpenType font after an 8-byte header; from offset 4 on, every
4-byte word is XORed with one key, which the size field gives away (it holds the font's length)."""
key = bytes(x ^ y for x, y in zip(data[4:8], struct.pack('>I', len(data) - 8)))
plain = bytearray(data[:4]) + bytearray(x ^ key[i % 4] for i, x in enumerate(data[4:]))
font = plain[8:]
assert font[:4] == b'OTTO', 'not a CFF OpenType font'
tables = {}
for k in range(struct.unpack_from('>H', font, 4)[0]):
tag, _, offset, length = struct.unpack_from('>4sIII', font, 12 + 16 * k)
tables[tag] = (12 + 16 * k, offset, length)
cff = tables[b'CFF '][1]
strings, size, end = cff_index(font, cff + cff_charstrings(font, cff))
hmtx, metrics = tables[b'hmtx'][1], struct.unpack_from('>H', font, tables[b'hhea'][1] + 34)[0]
order = list(range(len(strings)))
for a, b in pairs:
ga, gb = otf_glyph(font, tables[b'cmap'][1], a), otf_glyph(font, tables[b'cmap'][1], b)
assert 0 < ga < len(strings) and 0 < gb < len(strings) and ga < metrics and gb < metrics, (a, b, ga, gb)
order[ga], order[gb] = order[gb], order[ga]
ma, mb = hmtx + 4 * ga, hmtx + 4 * gb
font[ma:ma + 4], font[mb:mb + 4] = font[mb:mb + 4], font[ma:ma + 4]
# The CharStrings INDEX again with the items in the new order: the same length, so nothing after it moves.
body = b''.join(bytes(font[slice(*strings[g])]) for g in order)
first = strings[0][0]
assert first + len(body) == end
font[first:end] = body
at, base = first - (len(strings) + 1) * size, 1 # the offset array ends where the first item starts
for i, g in enumerate(order + [None]):
font[at + i * size:at + (i + 1) * size] = base.to_bytes(size, 'big')
if g is not None:
base += strings[g][1] - strings[g][0]
head = tables[b'head'][1]
struct.pack_into('>I', font, head + 8, 0)
for entry, offset, length in tables.values():
struct.pack_into('>I', font, entry + 4, otf_checksum(font[offset:offset + length]))
struct.pack_into('>I', font, head + 8, (0xb1b0afba - otf_checksum(font)) & 0xffffffff)
plain[8:] = font
return bytes(plain[:4]) + bytes(x ^ key[i % 4] for i, x in enumerate(plain[4:]))
def sarc_replace(data, new):
"""The SARC data with the members named in new (name -> bytes) replaced. The header, SFAT and SFNT stay; the
members are laid out again in their old order, each at an offset with at least the alignment its old one had."""
start = struct.unpack_from('<I', data, 12)[0]
out = bytearray(data[:start])
for name, node, lo, hi in sorted(sarc_nodes(data), key=lambda n: n[2]):
align = min(lo & -lo, 0x2000) if lo else 0x2000
out += bytes(-len(out) % align)
body = new.get(name, data[lo:hi])
struct.pack_into('<II', out, node + 8, len(out) - start, len(out) - start + len(body))
out += body
struct.pack_into('<I', out, 8, len(out))
return bytes(out)
COMMON = {'v121': '110', 'v143': '140'} # the version in the name of each game's Common archive
ARCHIVE = f'UI/LayoutArchive/Common.Product.{COMMON[args.version]}.Nin_NX_NVN.blarc.zs'
MEMBERS = {
'anim/ShortCutBG_00_In.bflan': ('C_CaptureBG_00', 'C_CaptureClearBG_00', 'P_CaptureClearBG_00'),
'anim/AutoBuilder_00_In.bflan': ('C_CaptureBG_00',),
}
KEY_TEXTURES = (('Nt_KeyTexA_00', 'Nt_KeyTexB_00'), ('Nt_KeyTexX_00', 'Nt_KeyTexY_00'))
KEY_GLYPHS = ((0xE040, 0xE041), (0xE042, 0xE043)) # A B, X Y in scft/External.bfotf
KEY_LETTERS = {'A': 'B', 'B': 'A', 'X': 'Y', 'Y': 'X'} # the face-button diamond around the action prompt
FONTS = sorted(p for p in files if re.fullmatch(r'Font/Font(_\w+)?\.Nin_NX_NVN\.bfarc\.zs', p))
# The message archive of each language: the newest version in the RomFS (Mals/USen.Product.121.sarc.zs on 1.2.1).
MALS = {}
for p in files:
m = re.fullmatch(r'Mals/(\w+)\.Product\.(\d+)\.sarc\.zs', p)
if m and int(m[2]) > MALS.get(m[1], (0, ''))[0]:
MALS[m[1]] = (int(m[2]), p)
MALS = sorted(p for _, p in MALS.values())
LAYOUT_MSG = 'LayoutMsg/DoCommand_00.msbt'
with (args.romfs / 'romfs.bin').open('rb') as image:
def read(path):
@@ -96,7 +339,8 @@ with (args.romfs / 'romfs.bin').open('rb') as image:
for name, (lo, hi) in sarc_members(pack).items():
d = zstd.ZstdDict(pack[lo:hi])
dictionaries[d.dict_id] = d
raw = read(ARCHIVE)
originals = {path: read(path) for path in [ARCHIVE] + (FONTS + MALS if args.xbox_prompts else [])}
raw = originals[ARCHIVE]
info = zstd.get_frame_info(raw)
dictionary = dictionaries[info.dictionary_id]
original = zstd.decompress(raw, zstd_dict=dictionary)
@@ -115,15 +359,60 @@ for member, panes in MEMBERS.items():
changed.append(name)
assert sorted(changed) == sorted(panes), (member, changed)
patched[member] = changed
layout = []
if args.aspect:
width, height = (int(v) for v in args.aspect.lower().split('x'))
spec = importlib.util.spec_from_file_location('ultrawide_layout', Path(__file__).with_name('ultrawide_layout.py'))
ultrawide = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ultrawide)
layout = ultrawide.apply(blarc, members, width, height)
diff = [i for i in range(len(original)) if original[i] != blarc[i]]
assert diff and all(any(lo <= i < hi for lo, hi in (members[m] for m in MEMBERS)) for i in diff), diff
data = zstd.compress(bytes(blarc), level=19, zstd_dict=dictionary)
assert zstd.decompress(data, zstd_dict=dictionary) == bytes(blarc)
assert zstd.get_frame_info(data).dictionary_id == info.dictionary_id
target = args.out / ARCHIVE
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
line = '\t'.join([ARCHIVE, str(len(data)), hashlib.sha256(data).hexdigest(), hashlib.sha256(raw).hexdigest()])
touched = [members[m] for m in (*MEMBERS, *layout)]
assert diff and all(any(lo <= i < hi for lo, hi in touched) for i in diff), diff
outputs = {ARCHIVE: bytes(blarc)} # RomFS path -> new contents (before compression)
if args.xbox_prompts:
bntx_swap(blarc, members['timg/__Combined.bntx'][0], KEY_TEXTURES)
letters = {}
for member, (lo, hi) in members.items():
if member.endswith('.bflyt'):
changed = bflyt_letters(blarc, lo, hi, KEY_LETTERS)
if changed:
letters[member] = changed
assert 'blyt/DoCommand_00.bflyt' in letters, 'no letter panes in blyt/DoCommand_00.bflyt'
outputs[ARCHIVE] = bytes(blarc)
assert FONTS, 'no Font/Font*.Nin_NX_NVN.bfarc.zs in the RomFS'
for path in FONTS:
frame = zstd.get_frame_info(originals[path])
bfarc = zstd.decompress(originals[path], zstd_dict=dictionaries.get(frame.dictionary_id))
lo, hi = sarc_members(bfarc)['scft/External.bfotf']
outputs[path] = sarc_replace(bfarc, {'scft/External.bfotf': bfotf_swap(bfarc[lo:hi], KEY_GLYPHS)})
assert MALS, 'no Mals/<language>.Product.<version>.sarc.zs in the RomFS'
messages = {}
for path in MALS:
frame = zstd.get_frame_info(originals[path])
mals = bytearray(zstd.decompress(originals[path], zstd_dict=dictionaries.get(frame.dictionary_id)))
lo, hi = sarc_members(mals)[LAYOUT_MSG]
msbt = bytearray(mals[lo:hi])
changed = msbt_letters(msbt, KEY_LETTERS)
assert len(changed) == sum(len(v) for v in letters.values()), (path, changed)
mals[lo:hi] = msbt
outputs[path] = bytes(mals)
messages[path] = len(changed)
lines = []
for path, contents in outputs.items():
raw = originals[path]
dictionary = dictionaries.get(zstd.get_frame_info(raw).dictionary_id)
data = zstd.compress(contents, level=19, zstd_dict=dictionary)
assert zstd.decompress(data, zstd_dict=dictionary) == contents
assert zstd.get_frame_info(data).dictionary_id == zstd.get_frame_info(raw).dictionary_id
target = args.out / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(data)
lines.append('\t'.join([path, str(len(data)), hashlib.sha256(data).hexdigest(), hashlib.sha256(raw).hexdigest()]))
(args.out / 'manifest.tsv').write_text('# path\tsize\tsha256\toriginal_sha256 (tools/make_romfs_patches.py)\n'
+ line + '\n')
print(json.dumps(dict(status='pass', path=ARCHIVE, members=patched, size=len(data), changed_bytes=len(diff))))
+ ''.join(line + '\n' for line in lines))
print(json.dumps(dict(status='pass', path=ARCHIVE, members=patched, changed_bytes=len(diff),
ultrawide_layout=dict(aspect=args.aspect, members=len(layout)) if args.aspect else None,
xbox_prompts=dict(textures=KEY_TEXTURES, letters=letters, messages=messages, fonts=FONTS)
if args.xbox_prompts else None)))
+140
View File
@@ -0,0 +1,140 @@
"""Fold the loose files of a shader cache into <cache>/shader-cache.pack and delete them.
The translator writes four files per shader into the cache (runtime/gfx_shader.c): <key>.spv, <key>.res.json,
<name>.use and in/<input id>.in, about 117,000 files after an install, which are slow to copy, back up, scan and sync.
totk.exe reads the pack first and the loose files after it, and keeps writing new translations as loose files, so
this tool can run again at any time the game is closed: it merges the existing pack with the loose files (a loose file
wins over a packed one of the same name) and writes the pack anew. The installer runs it after preparing the shaders.
Layout, little-endian (gfx_shader.c, section "cache pack"): b'TKSPACK1', u32 count, u32 0, then count entries of 48
bytes sorted by (kind, name): name (the 32 bytes the file name's 64 hex digits spell), u32 kind (1 .spv, 2 .res.json,
3 .use, 4 in/*.in), u32 size, u64 offset; then the data.
python tools\\pack_shader_cache.py <install folder>\\shader-cache [--keep]
--keep leaves the loose files in place. Close the game first: a running totk.exe has read the old pack's index.
"""
import argparse
import os
import re
import struct
import sys
from pathlib import Path
MAGIC = b'TKSPACK1'
PACK = 'shader-cache.pack'
KINDS = {'.spv': 1, '.res.json': 2, '.use': 3, '.in': 4}
NAME = re.compile(r'([0-9a-f]{64})(\.spv|\.res\.json|\.use|\.in)')
USE_SIZE = 20 # gfx_shader.c use_store: magic, version, three masks
def loose_files(cache):
"""{(kind, name): path} of the cache's loose files; *.in only in in/."""
found = {}
for folder, kinds in ((cache, (1, 2, 3)), (cache / 'in', (4,))):
if not folder.is_dir():
continue
with os.scandir(folder) as it:
for e in it:
m = NAME.fullmatch(e.name)
if not m or not e.is_file():
continue
kind = KINDS[m.group(2)]
if kind in kinds:
found[(kind, bytes.fromhex(m.group(1)))] = Path(e.path)
return found
def read_pack(path):
"""{(kind, name): (offset, size)} of an existing pack; {} when there is none or it fails a check."""
try:
f = open(path, 'rb')
except FileNotFoundError:
return {}
with f:
size = os.fstat(f.fileno()).st_size
head = f.read(16)
if len(head) != 16 or head[:8] != MAGIC:
print(f'{path}: not a cache pack, rebuilt from the loose files only', flush=True)
return {}
count = struct.unpack_from('<I', head, 8)[0]
raw = f.read(48 * count)
if len(raw) != 48 * count:
print(f'{path}: truncated index, rebuilt from the loose files only', flush=True)
return {}
out = {}
for i in range(count):
name, kind, length, offset = struct.unpack_from('<32sIIQ', raw, 48 * i)
if offset + length <= size:
out[(kind, name)] = (offset, length)
return out
def valid(kind, data):
return len(data) == USE_SIZE if kind == 3 else len(data) > 0
def pack(cache, keep=False):
cache = Path(cache)
out = cache / PACK
packed = read_pack(out)
loose = loose_files(cache)
keys = sorted(set(packed) | set(loose))
tmp = out.with_name(PACK + '.tmp')
index, written, skipped, offset = [], [], 0, 16 + 48 * len(keys)
old = open(out, 'rb') if packed else None
try:
with open(tmp, 'wb') as f:
f.write(bytes(offset)) # header and index, written once the offsets are known
for key in keys:
data = None
if key in loose: # a valid loose file, else the packed copy, else nothing
try:
data = loose[key].read_bytes()
except OSError:
pass
if data is not None and valid(key[0], data):
written.append(loose[key])
else:
data = None
if data is None and key in packed:
old.seek(packed[key][0])
data = old.read(packed[key][1])
if data is None or not valid(key[0], data):
skipped += 1
continue
f.write(data)
index.append(struct.pack('<32sIIQ', key[1], key[0], len(data), offset))
offset += len(data)
f.seek(0)
f.write(MAGIC + struct.pack('<II', len(index), 0) + b''.join(index))
f.flush()
os.fsync(f.fileno())
finally:
if old:
old.close()
os.replace(tmp, out)
if not keep:
for p in written:
p.unlink(missing_ok=True)
try:
(cache / 'in').rmdir() # only when empty
except OSError:
pass
return len(index), len(written), skipped, offset
def main():
ap = argparse.ArgumentParser(description='Fold a shader cache into shader-cache.pack.')
ap.add_argument('cache', type=Path, help='the shader-cache folder next to totk.exe')
ap.add_argument('--keep', action='store_true', help='keep the loose files')
a = ap.parse_args()
if not a.cache.is_dir():
sys.exit(f'{a.cache} is not a folder')
n, new, skipped, size = pack(a.cache, a.keep)
print(f'{a.cache / PACK}: {n} files, {size / 2**20:.0f} MiB ({new} loose files folded in'
f'{", " + str(skipped) + " unreadable or empty skipped" if skipped else ""})', flush=True)
if __name__ == '__main__':
main()
+2
View File
@@ -91,6 +91,8 @@ def main():
else:
print(f'package_build: {f} not in {build}, skipped')
n = copy_tree(build / 'romfs-patches', out / 'romfs-patches')
if (build / 'romfs-patches-xbox').is_dir(): # pad_layout = xbox_prompts (make_romfs_patches.py --xbox-prompts)
n += copy_tree(build / 'romfs-patches-xbox', out / 'romfs-patches-xbox')
n += copy_tree(exefs, out / 'exefs')
n += copy_tree(fonts, out / 'shared-fonts')
if not a.no_shader_cache and (build / 'shader-cache').is_dir():
+145
View File
@@ -0,0 +1,145 @@
# Adapted from MacNativeGames games/totk/scripts/ultrawide_layout.py @ 933b6a0 (MIT, Copyright (c) 2026
# MacNativeGames contributors). Used by tools/make_romfs_patches.py --aspect.
"""The game's UI layouts for a canvas wider than 16:9 (output_res = 3440x1440 in totk.ini), edited in place in the
decompressed UI/LayoutArchive/Common.Product.*.blarc (tools/make_romfs_patches.py --aspect).
The game lays its UI out on a 1920x1080 grid and maps that grid onto the canvas, so on a 21:9 canvas every layout comes
out 34% too wide. Here each layout's root pane is narrowed to 16:9 (scale x = 16/9 over the aspect), backgrounds that
must cover the whole screen are widened back, and the HUD's parts move out to the screen's corners. The pane list and
the distances are the ones the community TOTK-Ultrawide mod for emulators found for 1.0.0 .. 1.2.1 (GPL; only those
values are taken, no code; THIRD_PARTY_NOTICES.md). This is an independent implementation, checked against 1.4.3's
Common.Product.140: every pane is found by name and every pane field and animation key holds the value the edit
expects before it is written.
"""
import struct
# BFLYT pane sections (pan1, pic1, txt1, ...): name at +12 (24 bytes), translate x / y at +44 / +48, scale x / y at +68 / +72.
PANE_MAGICS = (b'pan1', b'pic1', b'txt1', b'wnd1', b'bnd1', b'prt1', b'ali1', b'scr1')
FIELD = {'x': 44, 'y': 48, 'scale_x': 68, 'scale_y': 72}
# Full-screen layouts whose root pane stays as wide as the canvas.
KEEP_ROOT = {'AppMenuBG_00', 'PauseMenuBG_00', 'ShopMenuParasailBG_00', 'ShortCutBG_00', 'TitleMenuBG_00', 'EventFade_00',
'EventFadeWipe_00', 'CameraShutter_00', 'PaAppPictureBookDetail_00', 'AppMenuOverlay_00'}
def edits(k, hud_corner):
"""(layout, pane, field, value) for the aspect: k = 16/9 over the aspect (the root's scale), w = how far a 1920-wide
layout's edge moves out in its own units (960 * (1 / k - 1)). OFF moves a pane out of sight."""
w, OFF = 960 * (1 / k - 1), 10000.0
wide = 1 / k
out = [
('AppAlbum_00', 'N_FadeInOut_00', 'scale_x', k), ('AppAlbum_00', 'N_Open_01', 'scale_x', wide),
*[('AppMap_00', f'N_BlurInOut_0{i}', 'scale_x', wide) for i in range(4)],
('AppMap_00', 'N_MiniMap_00', 'x', -270 * (1 - k)),
('AppMenuOverlay_00', 'N_FadeInOut_00', 'scale_x', k), ('AppMenuOverlay_00', 'N_FadeInOut_01', 'scale_x', k),
('AppMenuOverlay_00', 'N_Icon_00', 'scale_x', k), ('AppMenuOverlay_00', 'N_TitleOne_01', 'scale_x', k),
('AppOpenDemoWindow_00', 'P_BG_00', 'scale_x', wide), ('AutoBuilder_00', 'N_BG_00', 'scale_x', wide),
('CameraPointer_00', 'N_InOutScope_00', 'scale_x', wide), ('CameraPointer_00', 'N_InOutScope_00', 'scale_y', wide),
('CameraPointer_00', 'N_Type_01', 'scale_x', k), ('CameraPointer_00', 'Pa_CameraFocus_00', 'scale_x', wide),
('CameraPointer_00', 'S_Scissor_00', 'scale_x', wide), ('CameraPointer_00', 'W_Shadow_01', 'scale_x', wide),
('CameraSystemWindow_00', 'N_AlbumCon_00', 'scale_x', k), ('CameraSystemWindow_00', 'N_InOut_00', 'scale_x', wide),
('CameraSystemWindow_00', 'Pa_BtnL_00', 'scale_x', k), ('CameraSystemWindow_00', 'Pa_BtnR_00', 'scale_x', k),
('CameraSystemWindow_00', 'T_Text_00', 'scale_x', k),
('ChallengeLog_00', 'C_CaptureUp_00', 'x', OFF), ('ChallengeLog_00', 'C_CaptureUp_01', 'x', OFF),
('GameOver_00', 'P_BG_00', 'scale_x', wide),
('LoadingFade_00', 'N_Base_00', 'scale_x', wide), ('LoadingFade_00', 'N_Logo_00', 'x', w),
('LoadingFade_00', 'N_Status_00', 'x', w), ('LoadingFade_00', 'N_Tips_00', 'x', -w),
('LoadingFade_00', 'P_BG_00', 'scale_x', wide), ('LoadingFade_00', 'P_SideBGDeco_00', 'scale_x', k),
('LoadingFade_00', 'Pa_IconPlayer_00', 'scale_x', k), ('LoadingFade_00', 'Pa_LargeDungeonMap_00', 'scale_x', k),
('LoadingFade_00', 'Pa_LargeDungeonMap_01', 'scale_x', k), ('LoadingFade_00', 'Pa_Map_00', 'scale_x', k),
('LoadingFade_00', 'Pa_Map_01', 'scale_x', k),
('MessageLog_01', 'P_Capture_00', 'scale_x', wide), ('OwnedHorseList_00', 'N_BG_00', 'scale_x', wide),
('PaCommonBtnTitle_00', 'B_Hit_00', 'x', OFF), ('PaCommonBtnTitle_00', 'N_Cursor_00', 'x', OFF),
('PaLargeDungeonMap_00', 'P_BG_00', 'scale_x', wide), ('PaMessageBtn_00', 'B_Hit_00', 'x', OFF),
('PaRecipeCardList_00', 'P_BG_00', 'scale_x', wide),
('SaveLoadIcon_00', 'RootPane', 'x', -960 * (1 - k)),
('ShopMenu_00', 'N_BG_00', 'scale_x', wide), ('StaffRoll_00', 'N_BG_00', 'scale_x', wide),
]
# Menus framed by two side decorations, a header and a background: the frame stays at the screen's edges.
for layout, deco in (('PauseMenuOverlay_00', 'P_Deco{}_00'), ('SystemLoadList_00', 'P_Deco{}_00'),
('SystemOption_00', 'P_Deco{}_00'), ('SystemSaveLoad_00', 'P_Deco{}_00')):
out += [(layout, deco.format('L'), 'scale_x', k), (layout, deco.format('L'), 'x', -(675 + w) * k),
(layout, deco.format('R'), 'scale_x', k), (layout, deco.format('R'), 'x', (737 + w) * k)]
if layout == 'PauseMenuOverlay_00':
out += [(layout, 'P_FooterBase_00', 'scale_x', wide), (layout, 'S_DecoScissor_00', 'scale_x', wide)]
else:
out += [(layout, 'A_Title_00', 'scale_x', k), (layout, 'N_Header_00', 'scale_x', wide),
(layout, 'N_SystemBG_00', 'scale_x', wide)]
out += [('SystemActionGuide_00', 'A_Title_00', 'scale_x', k), ('SystemActionGuide_00', 'N_Header_00', 'scale_x', wide),
('SystemActionGuide_00', 'N_SystemBG_00', 'scale_x', wide),
('SystemActionGuide_00', 'P_DecoL_01', 'scale_x', k), ('SystemActionGuide_00', 'P_DecoL_01', 'x', -(675 + w) * k),
('SystemActionGuide_00', 'P_DecoL_02', 'scale_x', k), ('SystemActionGuide_00', 'P_DecoL_02', 'x', (675 + w) * k)]
if hud_corner: # the HUD in the screen's corners instead of the middle 16:9
right = 960 * (1 - k)
out += [(name, 'RootPane', 'x', right) for name in ('EnvironmentInfo_00', 'EventSkip_00', 'InstantTipsAppMenu_00',
'InstantTipsWithPict_00', 'InstantTips_00', 'NumDisplaySecond_00',
'NumDisplay_00', 'PickUpWin_00')]
out += [('Location_00', 'N_Small_00', 'x', -(388 + w)),
('PlayerStatus3D_00', 'N_ExtraStaminaTarget_01', 'x', -(745 + w)),
('PlayerStatus_00', 'N_DevilDemoTargetC_00', 'x', -(284 + w)),
('PlayerStatus_00', 'N_DevilDemoTargetLT_00', 'x', -(576 + w)),
('PlayerStatus_00', 'N_HeartGaugeTargetLT_00', 'x', -(576 + w)),
('PlayerStatus_00', 'N_HeartPos_00', 'x', -w),
('PlayerStatus_00', 'Pa_CompanionLife_00', 'x', -(840 + w))]
return out
def anim_edits(k, hud_corner):
"""(animation, byte offset, value the game ships, new value): floats inside BFLAN key tables."""
w = 960 * (1 / k - 1)
out = [('PaMapIconDragonTears_00_Zoom', 288, 1.08, 1.08 / k), ('PaMapIconDragonTears_00_Zoom', 300, 1.28, 1.28 / k)]
if hud_corner: # the minimap's slide-in keys end at x 765, the right edge of a 16:9 layout
out += [('AppMap_00_MiniMap', o, 765.0, 765 + w) for o in (872, 880, 1628, 1636, 3264, 3272, 3468, 3476)]
return out
def panes(blarc, lo, hi):
"""pane name -> absolute section offset, first occurrence, for the BFLYT at blarc[lo:hi]."""
assert blarc[lo:lo + 4] == b'FLYT', 'not a BFLYT'
head, count = struct.unpack_from('<H', blarc, lo + 6)[0], struct.unpack_from('<H', blarc, lo + 0x10)[0]
pos, out = lo + head, {}
for _ in range(count):
magic, size = bytes(blarc[pos:pos + 4]), struct.unpack_from('<I', blarc, pos + 4)[0]
if magic in PANE_MAGICS:
out.setdefault(bytes(blarc[pos + 12:pos + 36]).split(b'\0')[0].decode(), pos)
pos += size
assert pos <= hi
return out
def find_pane(found, layout, name):
"""The pane of that exact name (a version that renamed it stops the script)."""
if name not in found: raise SystemExit(f'{layout}: no pane {name}')
return found[name]
def apply(blarc, members, width, height, hud_corner=True):
"""Edits blarc (bytearray) in place for a width x height canvas. Returns the members it changed."""
k = (16 / 9) / (width / height)
assert k < 1, 'only for canvases wider than 16:9'
changed, layouts = set(), {}
unknown = sorted(n for n in KEEP_ROOT if f'blyt/{n}.bflyt' not in members)
if unknown: raise SystemExit(f'KEEP_ROOT names no layout: {unknown}')
def pane_offset(layout, pane):
member = f'blyt/{layout}.bflyt'
if member not in members: raise SystemExit(f'{layout}: no layout {member} in the archive')
if member not in layouts: layouts[member] = panes(blarc, *members[member])
changed.add(member)
return find_pane(layouts[member], layout, pane)
for member in sorted(m for m in members if m.startswith('blyt/') and m.endswith('.bflyt')):
if member[5:-6] in KEEP_ROOT: continue
at = pane_offset(member[5:-6], 'RootPane') + FIELD['scale_x']
have = struct.unpack_from('<f', blarc, at)[0]
if abs(have - 1.0) > 1e-3: raise SystemExit(f'{member[5:-6]} RootPane scale_x: {have}, expected 1.0')
struct.pack_into('<f', blarc, at, k)
# Every edit's value at k = 1 is the value the game ships (a scale of 1, the 16:9 position): checked before writing,
# so a version with other numbers stops here instead of getting wrong positions. OFF moves a pane away: not checked.
for (layout, pane, field, value), (_, _, _, shipped) in zip(edits(k, hud_corner), edits(1.0, hud_corner)):
at = pane_offset(layout, pane) + FIELD[field]
have = struct.unpack_from('<f', blarc, at)[0]
if value != 10000.0 and abs(have - shipped) > 1e-3: raise SystemExit(f'{layout} {pane} {field}: {have}, expected {shipped}')
struct.pack_into('<f', blarc, at, value)
for anim, offset, shipped, value in anim_edits(k, hud_corner):
member = f'anim/{anim}.bflan'
if member not in members: raise SystemExit(f'{anim}: no animation {member} in the archive')
lo, hi = members[member]
if offset + 4 > hi - lo: raise SystemExit(f'{anim}: too short for +{offset}')
have = struct.unpack_from('<f', blarc, lo + offset)[0]
if abs(have - shipped) >= 1e-4: raise SystemExit(f'{anim} +{offset}: {have}, expected {shipped}')
struct.pack_into('<f', blarc, lo + offset, value); changed.add(member)
return sorted(changed)
+17 -2
View File
@@ -109,7 +109,11 @@
# gpu_index = 0
# gpu_name = NVIDIA
# Game resolution: the size the game draws its picture and interface in (1280x720 to 7680x4320). Above 1920x1080 the
# game's texture heap and memory pool grow with it. Read at start.
# game's texture heap and memory pool grow with it. A size wider than 16:9, such as 3440x1440 on a 21:9 monitor, widens
# the camera's view to that shape and lays the HUD out for it. That is 1.4.3 only (1.2.1 keeps the 16:9 view), and the
# installer prepares the UI layout for four sizes: 2560x1080, 3440x1440, 3840x1600 and 5120x2160. Any other shape
# (16:10, 32:9) gets the camera's view but a 16:9 HUD, stretched over the picture, unless you build its layout with
# tools/make_romfs_patches.py --aspect WxH (wider than 16:9 only). Read at start.
# output_res = 1920x1080
# 3D render scale: the scene's size relative to the game resolution, game (5/6, the game's own 1600x900 at 1080p) or
# a percentage (25 to 400, at most 4096 pixels a side); above 100 supersamples the scene (anti-aliasing). Read at start.
@@ -118,6 +122,11 @@
# from 0 (off) to 100. Changes while the game runs take effect at once.
# upscaler = fsr
# sharpness = 80
# Picture shape when the window's shape differs from the game resolution's (a 16:9 picture in a 21:9 fullscreen
# window): fit, the default, keeps the shape, with black bars at the sides or above and below, so a 16:9 game in a wider
# window is no longer stretched as it used to be; stretch fills the window. Changes while the game runs take effect at
# once.
# aspect = fit
[language]
# Language: a Windows locale such as de-DE, or a Switch code (en-US, ja, zh-Hant, ...). Default: the Windows locale.
@@ -148,8 +157,13 @@
# rumble_triggers = 0
# Which gamepad button is Switch A: nintendo (by position: east A, south B, west Y, north X), xbox (south A, east B,
# west X, north Y, as an Xbox pad labels them; cross A on a PlayStation pad) or auto (nintendo for a pad with Nintendo
# labels, xbox for the others). The bind_ lines below apply on top. Changes while the game runs take effect at once.
# labels, xbox for the others). xbox_prompts: the buttons as nintendo, and the game's prompts swapped to the Xbox
# labels (A with B, X with Y; it loads romfs-patches-xbox, docs/controls.md, "Xbox button prompts"). The bind_ lines
# below apply on top. Changes while the game runs take effect at once; the xbox_prompts prompts at the next start.
# pad_layout = auto
# Gamepad stick dead zone in percent of full deflection (0 to 50). Raise it for a stick that drifts: inside the circle
# the stick reads centred, and the rest of its travel is scaled to start there.
# stick_deadzone = 8
# Mouse camera sensitivity (0 to 10; 0 = no mouse camera and no cursor capture).
# mouse_sens = 1
# Write the input of this run to a file that replays as a script (docs/controls.md, "Scripted input"). Default: off.
@@ -160,6 +174,7 @@
# A line replaces that button's defaults (pad and keyboard); buttons without a line keep them. Stick directions take keys
# only. Invalid entries are logged and skipped. Changes while the game runs take effect at once.
# pad names: south east west north l r zl zr start back lstick rstick dpad_left dpad_up dpad_right dpad_down
# lpaddle1 rpaddle1 lpaddle2 rpaddle2 (the back buttons of an Xbox Elite, DualSense Edge or Steam Controller)
# key names: a-z 0-9 f2-f7 f9 f12 num0-num9 space tab enter backspace lshift rshift lctrl rctrl lalt capslock insert
# delete home end pageup pagedown up down left right num_add num_sub num_mul num_div num_decimal minus equals
# lbracket rbracket semicolon apostrophe comma period slash backslash grave mouse_l mouse_r mouse_m mouse_x1 mouse_x2