Compare commits

...
Author SHA1 Message Date
ayozetr 726d7b3760 Launcher: version 0.70-linux.5 2026-11-12 00:22:55 +00:00
ayozetr d68ff3987e Atlas: remove runtime_atlas_build.h, which nothing includes
The header came with Hunter's Atlas (6621a1f0) and holds the build
description of the Windows release it was taken from (windows-v1.8.1).
No source, script or test includes it or reads BB_ATLAS_BUILD_JSON; the
build and the tests are the same without it.
2026-11-09 22:28:27 +00:00
ayozetr 48fc69fb59 Patches: L3 run-jump, from bbhost; credits for the latest fixes
Kyo's L3 run-jump patch for 1.09, as droogie's bbhost carries it
(patches/l3-run-jump.toml, byte for byte, each site checked against the
eboot first). While running with Circle held, L3 makes the Rolling
request a Circle press makes, and the player's SpEffect 390 counts as on
for three frames, so the script takes its own run-jump: L3 jumps as
letting go of Circle and pressing it again does, and Circle is
unchanged. Its code goes in the zero tail of the code segment
(0x054BF000 and 0x054BF080, PS4 addresses) and keeps its window in one
byte of the zeroed tail of .bss (0x05AD3D00); it is RIP- and
register-relative, so it runs wherever the eboot is loaded. Not for the
Nightreign Movement scripts, where L3 is their surge sprint.

Off by default: tweak_l3_run_jump in bbport.ini, the launcher's Cheats
page and the in-game menu, in every language.

Credits (docs/CREDITS.md and the launcher's About page, in every
language): droogie's entry gains the L3 run-jump, and Asanilo (the work
submission count and sampler fixes), AlexanderDzhoganov (BBSC's
spectating after a wipe) and nullsumlabs (unknown sysctl names) are in.
2026-11-09 22:11:46 +00:00
ayozetr ce84be68dc Window: keep keys and typed text for the name dialog on the Deck
With a Steam Deck or Steam Controller as the gamepad, keyboard and mouse
events are dropped as echoes of its own controls (Steam's desktop
layout), except while the port's menu is open. That also dropped them
while the game's text dialog was open: Steam's on-screen keyboard types
there as text input and a keyboard plugged into the Deck as keys, so in
Game Mode the character's name could not be typed and the dialog could
not be left with Escape (upstream issue #443, and a report on Reddit).
They now reach the dialog; the port's own on-screen keyboard still takes
the gamepad. BbOverlay::TextPromptOpen says when the dialog is open.
2026-11-09 22:07:00 +00:00
ayozetr 61a3c3f4a7 Docs: frame rate drops until a reload, and the mouse in menus
TROUBLESHOOTING.md gains two sections: the drops on NVIDIA cards that
lasted until a reload (#42), what 0.70-linux.4 changed, how to tell them
from a place that is simply heavy, BB_MIRROR_VRAM_ONLY and which log to
attach; and the keyboard and mouse in the game's menus: the left button
chooses, the others keep their bindings (the right one L2, for the
storage box), Escape goes back.
2026-11-09 22:04:38 +00:00
ayozetr 23191f2a81 Overlay: say how much VRAM other programs took, not "VRAM -552 MB"
The frame rate line adds how much less video memory the game has than
earlier in the session, when other programs (a browser, a video player)
take 150 MB or more. It read "VRAM -552 MB (other apps)", which looked
like a negative figure. It now reads "VRAM: 552 MB taken by other
apps", in every menu language.
2026-11-09 22:04:14 +00:00
ayozetr 65e1a94db1 Exit to desktop: notice the title through the game's menu flow
With online play on, the game came back to the title after Exit Game
without opening the intro logos or looking its title menu's texts up
again, the two things the port waited for, and closed only when a new
game was started from there (#46). The menu pointer already reads the
game's menu flow, whose +0xb11 is zero at the title menu: the change
from the world to the title now calls runtime_quit_at_title too, which
does nothing unless Exit to desktop is armed. The menu pointer's header
now says the right button keeps its binding in menus.
2026-11-09 22:04:14 +00:00
ayozetr 614ac83cb1 Kernel: answer an unknown sysctl name with ENOENT
A sysctl name the port does not know stopped the game (exit 21). It now
answers ENOENT, as FreeBSD does for a name it does not know, and logs the
first few with their mib. From the nullsumlabs fork
(bloodborne_pc-deck-optimized), which took it from Awakenchan's Windows
port.
2026-11-09 21:40:56 +00:00
ayozetrandAlexander Dzhoganov 5f0580bd43 BBSC: end a guest's spectating when the host reloads after a wipe
From Alexander Dzhoganov's pull request to bbport
(deadinside28/bloodborne_pc#418, refs CLSAMG22/BBSC#44). After a boss
wipe the host respawned while the guest stayed a spectator, its area
unloaded, seeing only the host's character moving in the air. The life
packets can miss a wipe when the host was already dead as the guest's
death began, or wakes in another map.

The host's position reports are now a second signal: one that arrives
by a load after the guest went down, with the boss not killed, means
the host reloaded. The guest reloads into the host's reported map when
it differs from its own, applying the host's world state first, as a
follow does.

Co-Authored-By: Alexander Dzhoganov <[email protected]>
2026-11-09 21:35:35 +00:00
ayozetrandAsanilo f16e9dec02 Texture cache: free evicted samplers after the frame that uses them
From Asanilo's pull request to bbport (deadinside28/bloodborne_pc#442,
fixing #441). The sampler cache's first collection pass could pick
entries just bound for the frame being recorded, and destroying them at
once left render commands pointing at a destroyed sampler. Evicted
samplers now go through Scheduler::DeferOperation and live until the
current render tick completes; their cache entries and LRU slots are
still removed at once.

tests/test_sampler_gc.py covers the first collection, entries used after
warm-up, cold entries and repeated use.

Co-Authored-By: Asanilo <[email protected]>
2026-11-09 21:35:34 +00:00
ayozetrandAsanilo 6f49316ca1 Command processor: publish submission counts under the queue lock
From Asanilo's pull request to bbport (deadinside28/bloodborne_pc#440,
fixing #439). SubmitGfx and SubmitAsc made a queued task visible before
updating its count and generation, so the decoder could finish the
previous task and see zero submissions while the new one was still
queued, and an idle callback in that window could accept the old
generation. The count, generation and their flags are now updated while
the queue lock is held; notifications stay outside the locks, and the
lock order (queue, then submit) is the consumer's.

tests/test_submission_publish.py covers the ordering.

Co-Authored-By: Asanilo <[email protected]>
2026-11-09 21:35:33 +00:00
ayozetr c34fe3a97c Launcher: version 0.70-linux.4 2026-11-09 12:33:12 +00:00
ayozetr 23730cd9c1 Layer: keep VRAM mirrors in VRAM only on cards over 4 GiB
When VRAM was full, VMA put a new mirror in system memory, and there it
stayed until its blocks were retired: the GPU read that geometry over
the bus until a reload. An RTX 4060 fell to 17-27 FPS in the same places
until the game was reloaded (#42), and BB_MIRROR_VRAM_ONLY=1 fixed it.
A mirror refused for lack of room is made again later instead.

That is now the default on cards with more than 4 GiB. Small cards keep
the system memory fallback: VRAM only had a 2 GB laptop reading its
geometry over the bus at 8-29 FPS. BB_MIRROR_VRAM_ONLY=1 or 0 forces
either, and the log says which one a run uses.
2026-11-09 12:33:12 +00:00
ayozetr f48d860e7f Credits: the latest contributors and testers
In docs/CREDITS.md and the launcher's About page, in every language:
Wooloomooloo2 for the game starting on integrated Intel Arc/Xe GPUs,
SrShadowy for the VRAM mirror compaction, eberhardweber for the AMD FX,
Wayland fullscreen and Gear Lever reports and tests, GitBire for the
storage box with keyboard and mouse, npc-xc for the NVIDIA frame rate
drop logs, and Nahnahchi's entry with those drops and Exit to Desktop
with online play.
2026-11-09 11:59:24 +00:00
ayozetrandSrShadowy c1020650e9 Layer: compact VRAM mirrors in the default run
From the SrShadowy fork. Compaction makes a mirror again over its valid
blocks only, giving back the VRAM the rest held. In 0.7's default run
it freed nothing: its mirrors are about 7.8 MiB each, and a mirror
needed a fixed 8 MiB of waste before it was considered.

The threshold now follows the mirror's size, and the segments a mirror
is cut into come from gpu/layer/layer_mirror_policy.h (heads and tails
always come off; a hole between valid blocks pays for the mirror it
needs). One pass takes a bounded share of the waste, so its copies
spread over the following seconds, and waste nothing could free is
looked at again after a while, as mirrors age into eligibility. A
retired mirror's age is forgotten on every retire, so a new mirror at
the same start does not inherit it. A pass that frees nothing says
which filter stopped it.

Seen with frame rate drops on NVIDIA cards (#42). The policy has its own
test (tests/test_layer_mirror_policy.cpp, run by build.sh --test).

Co-Authored-By: SrShadowy <[email protected]>
2026-11-09 11:55:29 +00:00
ayozetr 98750d0687 AppImage: give appimagetool the version
appimagetool writes the desktop entry's X-AppImage-Version itself, from
the VERSION environment variable, and replaced the one the build had
added with "UNKNOWN" (#49). The build now exports VERSION instead.
2026-11-08 01:21:34 +00:00
ayozetr 0b3dac11d7 Launcher: version 0.70-linux.3 2026-11-08 01:15:44 +00:00
ayozetr 5041586f9a AppImage: version and description for AppImage managers
Gear Lever showed the AppImage's version as "UNKNOWN" (#49): its desktop
entry had no X-AppImage-Version and there was no AppStream metainfo.
The build now writes both: the version is taken from the launcher's
VERSION (bbport_update.py), so it is kept in one place, and the
metainfo carries the name, summary, description, licence, links and
that version. Both pass desktop-file-validate and appstreamcli validate.
2026-11-08 01:15:38 +00:00
ayozetr e77bce746f Menu pointer: clicks reach the quick menu while the Atlas tile shows
The pointer left a click alone while the port's overlay was open, and
that check was also true whenever Hunter's Atlas's Map tile showed in
the game's quick menu. Clicks on Status, System, Inventory and the rest
did nothing there; only the tile's Map opened.

The pointer now steps aside only when the overlay or the open map takes
the input, and leaves a click on the tile, with its release, to the
tile. With BB_MENU_POINTER_FILE set, each click is traced: whether the
pointer took it, why not, and the button it pressed.
2026-11-08 01:15:30 +00:00
ayozetr d1dea1b8e4 Menu pointer: the right mouse button is L2 in the game's menus
In a game menu the right button went back, and the mouse's other
buttons did not reach the game at all, so with keyboard and mouse
nothing could press L2 or R2 there. The storage box switches to the
storage with them (#44), and its prompt shows the right button for it:
pressing it closed the box instead.

Only the left button is the pointer's now. The others keep their
bindings in menus as well (the right one L2, the side ones R2 and L1),
as the game's prompts show them; Escape goes back. The test file's
"right" action is "back".
2026-11-08 01:15:30 +00:00
ayozetr 6796fbbd13 Window: ask for fullscreen again when Wayland drops it at start
On Wayland a program cannot place its window. With several monitors
KWin could map the game on the one SDL reports as primary, move it to
the monitor chosen in the launcher and leave it there as a plain
window (#39); asked again, fullscreen holds, as KDE's own shortcut
showed. During the first 10 seconds the port now asks again, at most
three times and a second apart, and logs each try. After that the
desktop decides, so its own shortcuts keep working.

TROUBLESHOOTING.md: choosing the monitor, the retry and
SDL_VIDEO_DRIVER=x11 as the fallback.
2026-11-07 12:57:45 +00:00
ayozetr badd460277 Texture cache: name the images when an overlap resolve fails
The warning said only "Image overlap resolve failed". It now gives the
image the game asked for and the one dropped for it (address, size,
dimensions, format, levels and layers), and whether the dropped image
held what the GPU drew: then its contents are lost and the new image is
read from guest memory. Issue #25 had two of these warnings around a
garbled texture that went away after a reload.
2026-11-07 11:56:21 +00:00
ayozetr 1d0bbec978 Launcher: version 0.70-linux.2 2026-11-04 11:11:38 +00:00
ayozetrandMark Hall 843424de13 Guest memory: allow device-local host types on single-pool GPUs
On integrated Intel Arc/Xe with Mesa's ANV driver every memory type is
device-local and host-pointer imports are allowed only into those, so no
type was found and the game stopped at startup with exit code 23. A second
pass now accepts a device-local host-visible type when no system-memory
type is allowed; the search is unchanged on other GPUs.

From deadinside28/bloodborne_pc#429.

Co-Authored-By: Mark Hall <[email protected]>
2026-11-04 11:11:26 +00:00
ayozetr 21e1ce4224 Game menu: Exit to desktop saves first in the world
Exit to desktop closed the window at once, so whatever the game had not
saved yet was lost (#25). In the world the System menu's entry now runs the
game's own Quit Game: its question becomes "Quit the game and return to the
desktop?", the game saves and goes back to the title, and the runtime closes
it there (the intro logos or the title menu, once no save data is mounted).

Answering No leaves the quit armed for two minutes at most; the game's own
Quit Game disarms it. On the title screen the entry still closes the game
at once, as there is nothing to save there.
2026-11-04 11:11:26 +00:00
ayozetr 498a37d24b Runtime: run the game's movbe on processors without MOVBE
The game's code was built for the PS4's CPU and has 987 movbe. AMD's FX
processors of the Piledriver generation have every other instruction the
game uses but not this one, so the game stopped with SIGILL (exit code 132)
at the first one, while reading a file header (#39).

The SIGILL handler now decodes the movbe at the faulting address, does the
load or store with its bytes swapped and resumes after it. On a processor
with MOVBE this never runs. The CPU check no longer refuses to start
without MOVBE and says in the log that the port runs them; the log also
counts them every 10 s (a few thousand a second while loading, about ten a
second while playing).

BB_MOVBE_TRAP=file turns the movbe listed by tools/movbe_sites.py into ud2,
so the emulation can be tested on any processor. tests/test_movbe.c checks
the forms the game uses.
2026-11-04 11:11:26 +00:00
ayozetr e8c7eaaf9c Players' guide: brought up to 0.70
The guide still described 0.51 in a few places:

- FAQ: The Hunter's Dream has no seamless co-op in 0.70 (summons are the
  normal game's), and no bridge relays there; the ports answer now says
  what UPnP, BBSC's relay and Tailscale each cover. New answers on the
  in-game map (preparing it, opening it with the quick menu, M or R3)
  and on FSR 4.1.2 (AMD's installer or amdxcffx64.dll, plus the FSR 4.1.x
  DLL, and the model in the port's menu).
- Troubleshooting: DLSS failures as 0.70 logs them ("DLSS: unavailable
  (...)"), Frame Generation on RTX 40 and newer, without the 0.4-linux.1
  note and a log line that no longer exists.
- Getting a log: the first line as 0.70 writes it.
- Online: the BBSC switch is "Turn on BBSC", as the launcher says.
2026-11-03 21:54:23 +00:00
50 changed files with 2167 additions and 153 deletions

No files matched your search

+5
View File
@@ -118,6 +118,8 @@ if [[ ${1:-} == --test ]]; then
out/pad-atlas-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_runtime.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/runtime-test
out/runtime-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -Isrc tests/test_movbe.c src/runtime_movbe.c -o out/movbe-test
out/movbe-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_file_mods.c src/runtime_remote.c -o out/file-mods-test
out/file-mods-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_sema.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/sema-test
@@ -147,4 +149,7 @@ if [[ ${1:-} == --test ]]; then
rm -rf out/atlas-features && out/atlas-features-test out/atlas-features
"${CXX:-c++}" "${atlas_cxx[@]}" tests/test_atlas_i18n.cpp -o out/atlas-i18n-test
out/atlas-i18n-test
# When a mirror is made again over its valid blocks only (gpu/layer/layer_mirror_policy.h).
"${CXX:-c++}" -std=c++20 -O2 -g -UNDEBUG -Wall -Wextra -Werror -Igpu tests/test_layer_mirror_policy.cpp -o out/layer-mirror-policy-test
out/layer-mirror-policy-test
fi
+10 -3
View File
@@ -49,7 +49,10 @@ Obrikash and noelmiller. What each one did:
motion vectors without NaN.
- **[Classmg22](https://github.com/Classmg22)** (CLS_AMG): BBSC, Seamless Co-op.
- **[Wozzardman](https://github.com/Wozzardman)**: the server for a group's own co-op, and BBSC's seamless summon.
- **[droogie](https://github.com/droogie)**: The Hunter's Dream server, and fast loading (bbhost).
- **[droogie](https://github.com/droogie)**: The Hunter's Dream server, fast loading, and the L3 run-jump patch (Kyo's) as bbhost carries it.
- **[Asanilo](https://github.com/Asanilo)**: two GPU fixes: work submission counts, and samplers freed after the frame that uses them.
- **[AlexanderDzhoganov](https://github.com/AlexanderDzhoganov)**: BBSC: the guest no longer stays a spectator after a boss wipe.
- **[nullsumlabs](https://github.com/nullsumlabs)**: an unknown system value (sysctl) no longer stops the game.
- **[fhody125-web](https://github.com/fhody125-web)**: NoDreamForHunter, the base of BBSC's game server.
- **[MikeTat1988](https://github.com/MikeTat1988)**: Hunter's Atlas, the fullscreen in-game map.
- **[IFreemz](https://github.com/IFreemz)**: the FSR 4 model without trails.
@@ -67,7 +70,8 @@ Obrikash and noelmiller. What each one did:
- **[Mrsuss60](https://github.com/Mrsuss60)**: the gestures menu from the keyboard, a deleted
image's slot, and the log of failed memory reservations.
- **[SrShadowy](https://github.com/SrShadowy)**: the on-screen keyboard for the character's name,
and the shader compiling notice.
the shader compiling notice, and compacting the memory copies in VRAM.
- **[Wooloomooloo2](https://github.com/Wooloomooloo2)**: the game starting on integrated Intel Arc/Xe GPUs.
- **[Qiming-Liu](https://github.com/Qiming-Liu)**: the Simplified Chinese translation.
- **[andrecabralgit](https://github.com/andrecabralgit)**: the game's audio at full level.
- **[Jgamrot](https://github.com/Jgamrot)**: the in-game menu's checkboxes and sliders.
@@ -98,7 +102,7 @@ Obrikash and noelmiller. What each one did:
- **[n3vrs0ft](https://github.com/n3vrs0ft)**: the "Transaction failed" crash on Seamless Hunt, the logs that stopped a BBSC guest from following a stranger's world, and testing BBSC 0.0.2 with a Windows player.
- **[cappp](https://github.com/cappp)**: the "Transaction failed" crash on Seamless Hunt.
- **[simo1730](https://github.com/simo1730)**: the Trophies page not finding the dump's files.
- **[Nahnahchi](https://github.com/Nahnahchi)**: logs and tests of a GPU freeze in a boss fight on NVIDIA, a crash in Yahar'gul, stretched models, a crash on the Steam Deck, effects turning black, rainbow textures and crashes in the game's memory manager.
- **[Nahnahchi](https://github.com/Nahnahchi)**: logs and tests of a GPU freeze in a boss fight on NVIDIA, a crash in Yahar'gul, stretched models, a crash on the Steam Deck, effects turning black, rainbow textures, crashes in the game's memory manager, frame rate drops on NVIDIA, and Exit to Desktop with online play.
- **[thomasgarafano1](https://github.com/thomasgarafano1)**: Hunter's Atlas not preparing the map with a GOTY dump, and testing the fix.
- **[TacticalBill](https://github.com/TacticalBill)**: marking trophies already earned on the PS4.
- **[holydingus](https://github.com/holydingus)**: BBSC's items not given to a new character.
@@ -107,3 +111,6 @@ Obrikash and noelmiller. What each one did:
- **[BlazinKarp](https://github.com/BlazinKarp)**: the Steam overlay in the game.
- **[molnar-a](https://github.com/molnar-a)**: a crash on the Steam Deck after lamp travel, and testing the fix.
- **[Aman9das](https://github.com/Aman9das)**: asking for bbport 0.6's keyboard layout.
- **[eberhardweber](https://github.com/eberhardweber)**: logs and tests of AMD FX processors, fullscreen on Wayland with several monitors, and the AppImage's version in Gear Lever.
- **[GitBire](https://github.com/GitBire)**: the storage box with keyboard and mouse.
- **[npc-xc](https://github.com/npc-xc)**: logs of frame rate drops on NVIDIA.
+6 -4
View File
@@ -55,7 +55,7 @@ at its start (`Environment:` in the log) and warns about `BB_*` names it does no
`=1: ...` / `=0: ...` / `=N: ...` say what that value does; `set: ...` means any value
turns it on (even `0`).
## Settings (106)
## Settings (107)
What players and the launchers set. The menu, bbport.ini and the launcher write most of them; set one by hand to override them.
@@ -98,6 +98,7 @@ What players and the launchers set. The menu, bbport.ini and the launcher write
| `BB_GAMEPAD` | =SDL GUID or part of the name: the controller to use (the launcher's) |
| `BB_GAME_DIR` | the game's folder (with eboot.bin) |
| `BB_GAME_MENU` | =0: no port pages in the game's System menu |
| `BB_GESTURES_NO_MOTION` | =1: the gestures menu without the motion-control icons (Dommo's mod; the launcher's switch) |
| `BB_GPU_ID` | =N: the GPU to use, by Vulkan index (the launcher's) |
| `BB_GPU_PROFILE` | =1: GPU profiler: time per labelled pass in the log (the launcher's) |
| `BB_HDR` | =1: HDR output allowed |
@@ -155,7 +156,7 @@ What players and the launchers set. The menu, bbport.ini and the launcher write
| `BB_SHADNET_SERVER` | the shadNet server |
| `BB_SHADNET_TOKEN` | the shadNet account token (the launcher's) |
| `BB_SHADNET_WEBAPI` | the shadNet web API server |
| `BB_SKIP_CPU_CHECK` | =1: skip the check of the CPU's bsr/bsf behaviour before the game starts |
| `BB_SKIP_CPU_CHECK` | =1: skip the check of the processor's instructions (AVX, BMI1, LZCNT...) before the game starts |
| `BB_SKIP_GAME_CHECK` | =1: start with game files that did not pass the check |
| `BB_STEAM_INPUT_X11` | =0: keep the native Wayland window when Steam made a virtual controller for the game (default: Xwayland, so Steam Input reaches it) |
| `BB_TEXTURE_QUALITY` | texture quality over bbport.ini's texture_quality |
@@ -318,7 +319,7 @@ Defaults to change on a machine where they misbehave, workarounds, and switches
| `BB_MIRROR_POOL_MB` | =N: retired mirrors kept for new ones |
| `BB_MIRROR_REPROMOTE_SECONDS` | =N (default 0): blocks sent back are not copied again that soon |
| `BB_MIRROR_UNBOUND_SECONDS` | =N (0 never): blocks no binding named for N seconds leave VRAM |
| `BB_MIRROR_VRAM_ONLY` | =1: mirrors in VRAM or not at all |
| `BB_MIRROR_VRAM_ONLY` | =1 / 0: mirrors in VRAM or not at all, or in system memory too when VRAM is full (default: VRAM only on cards over 4 GiB) |
| `BB_MOTION_LARGE` | =0: bone palettes of 16-64 KiB (the large beasts) get camera motion vectors only, not object motion |
| `BB_MOTION_VERTICES_MI` | =N (1-8): object motion vertex buffer in Mi vertices |
| `BB_NET_MODULE` | =path: the online module to load (default gpu/libbbnet20.so beside bb-probe) |
@@ -410,7 +411,7 @@ Defaults to change on a machine where they misbehave, workarounds, and switches
| `BB_VULKANINFO` | path of vulkaninfo for the launcher |
| `BB_WORK_RETIRED` | =0: the recording thread and fences do not wait for retired work |
## Diagnostics (281)
## Diagnostics (282)
Logs, dumps, traces, probes and experiments, for finding bugs. Most cost frame time; none is meant for playing.
@@ -572,6 +573,7 @@ Logs, dumps, traces, probes and experiments, for finding bugs. Most cost frame t
| `BB_MOD_TRACE` | =1: every file a mod replaces |
| `BB_MOTION_SELECT_LOG` | =1: each G-buffer vertex shader once with the motion vector selection |
| `BB_MOTION_TRACE` | =file: the frame after the file appears prints its motion draws |
| `BB_MOVBE_TRAP` | =file: the movbe at the image offsets listed there (tools/movbe_sites.py) made ud2 and run by the port, as on a processor without MOVBE |
| `BB_MSG_TRACE` | =1: the game's text lookups per 2 s by category (#255) |
| `BB_NGX_LOG` | =1 / 2: NVIDIA NGX log into the port's (2: verbose) |
| `BB_NULL_GPU` | =1: no GPU work at all (tests) |
+1 -1
View File
@@ -67,7 +67,7 @@ items and echoes it gets, and enemies get stronger for every guest. It runs insi
online module (`gpu/bbnet20/bbsc`, BBSC 0.0.2 from bbport 0.7). It is new, and its own author
marks much of it as untested.
1. In the launcher's **Multiplayer → Online** tab, **Enable BBSC** in the **Seamless co-op (BBSC)**
1. In the launcher's **Multiplayer → Online** tab, **Turn on BBSC** in the **Seamless co-op (BBSC)**
group (on by default). While it is on, BBSC's bells replace the game's; turned off, the game's
come back the next time the character is loaded (the save is copied to `save-backups` first).
**BBSC settings**, folded under it, has BBSC's own options: what the guest shares with the host,
+41 -9
View File
@@ -27,17 +27,16 @@ To move a save, close the game, back up both folders, and copy the `savedata` fo
## DLSS is "not supported by this GPU"
The log says why, on a line starting with `DLSS:`.
The log says why, on a line `DLSS: unavailable (...)`; the game then uses another upscaler.
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's own driver; it does not work with
NVK.
- The 0.4-linux.1 AppImage ships a modified copy of NVIDIA's DLSS library, which the driver
refuses (`Feature requirement query failed: 0xbad00012`): update to 0.5-linux.1 or newer.
NVK (`not an NVIDIA GPU`, or `this GPU or driver does not support DLSS`).
- `NVIDIA NGX initialization failed`: update the driver, and check that NVIDIA's CUDA/UVM module
is loaded (`sudo modprobe nvidia_uvm`; the `nvidia-modprobe` package loads it when needed). With
Secure Boot, a module signing key that is not enrolled stops it (`Key was rejected by service`).
- DLSS Frame Generation needs a GeForce RTX 40 or newer; on older cards the log says so and DLSS
itself still works.
- From the repository, DLSS needs `bash tools/build_dlss.sh` once after `build.sh`.
- If the log says `Device creation with DLSS extensions failed`, NVIDIA's CUDA/UVM
module is missing: the game then uses FSR 3.1. Load it with `sudo modprobe nvidia_uvm` (the
`nvidia-modprobe` package loads it when needed); with Secure Boot, a module signing key that
is not enrolled stops it (`Key was rejected by service`).
## Spanish shows the Latin American text (or the other way round)
@@ -52,6 +51,27 @@ On a 6 GB card the texture cache can run out of video memory: the log then repea
a lower output resolution, lower **Model detail** in the game's *Display* settings, and close
other programs that use the GPU (browsers, video players, other games).
## The frame rate drops in some places and stays low until a reload
Seen mostly on NVIDIA cards (#42): the game runs at its usual rate, then in some places drops
(to 17-35 FPS) and stays there until a lamp travel or a restart. When video memory was full, a
copy of the game's memory could land in system memory and stay there, and the GPU read it over
the bus. From 0.70-linux.4, on cards with more than 4 GB those copies stay in video memory, and
one that does not fit is made again later; the log says so at start (`Layer memory: mirrors in
VRAM only`). `BB_MIRROR_VRAM_ONLY=0` under **Advanced → Developer → Extra variables** brings the
old behaviour back, and `=1` turns it on for a card of 4 GB or less.
A place that is simply heavy is different: it is slower from the moment you get there, and the
same after a reload. If a drop stays until a reload, turn on **Save the log and statistics to a
file** and attach the log of that whole session to the issue.
## Keyboard and mouse in the game's menus
In the game's menus the mouse points at items and the **left button** chooses them. The other
buttons keep what they do in play, as the game's own prompts show: the **right button** is L2
(the storage box switches to the storage with it), the side buttons R2 and L1. **Escape** goes
back.
## The picture is stretched on an ultrawide screen
Choose the screen's shape in **Graphics → Display → Aspect ratio** (*Auto* follows the output
@@ -60,11 +80,23 @@ and the HUD keeps to the screen's edges; it changes live, in the game's *Display
**Output resolution** *Auto* is the monitor's own; any other size written as `output_res=WxH` in
bbport.ini works too.
## Fullscreen opens on the wrong monitor, or drops back to a window
With several monitors, choose the one to play on in **Display → Monitor**: the port's *Primary*
is the one the system reports first, which on Wayland is not always the one KDE or GNOME show as
primary. On Wayland a program cannot place its own window, so the compositor may open it on
another monitor first and then move it; when that leaves it as a plain window, the port asks for
fullscreen again during the first seconds (the log says
`Display: the compositor left the window out of fullscreen, asked again`). If it still ends up
windowed, add `SDL_VIDEO_DRIVER=x11` under **Advanced → Developer → Extra variables**: through
Xwayland the window places itself.
## Exit to desktop
**Exit to desktop** is in the game's System menu and on the title screen, from bbport 0.7. The
*Quit to Desktop* switch of earlier Linux versions is gone with it, and a third-party *Exit Game*
patch is no longer needed.
patch is no longer needed. In the world it asks first and goes through the game's own Quit Game,
so your progress is saved, and the game closes once it is back at the title.
## The port's menu opens by accident with L3 + R3
+26 -5
View File
@@ -26,16 +26,37 @@ It depends on the server:
- On **Seamless Hunt** (or your own server with BBSC's game server), seamless co-op works fully:
summons across areas and up to 5 hunters.
- On **The Hunter's Dream**, BBSC keeps the session together, but summons are the normal game's:
both hunters must be in the **same area**, the area's boss must still be alive in the host's world,
and your levels must be within range.
- On **The Hunter's Dream** there is no seamless co-op: summons are the normal game's, with the
Beckoning Bell and the Small Resonant Bell. Both hunters must be in the **same area**, the area's
boss must still be alive in the host's world, and your levels must be within range (or use the
same password).
[Seamless co-op on each server](ONLINE.md#seamless-co-op-bbsc).
## Do I need to open ports on my router?
No. With seamless co-op (BBSC) on, Seamless Hunt relays players who cannot reach each other
directly, and The Hunter's Dream's bridge relays by itself. Only [your own server](SERVER.md) needs a few ports, through Tailscale.
Usually not: with **UPnP** on (the default) the game opens its port on the router by itself, and
with seamless co-op (BBSC) on Seamless Hunt relays players who cannot reach each other directly. A
router without UPnP, or a firewall on your computer, needs UDP 3658 opened
([ports](ONLINE.md#ports)). With [your own server](SERVER.md), Tailscale takes care of it.
## How do I use the in-game map?
In the launcher, **Mods → Map (Hunter's Atlas)**: turn on **In-game map** and press **Prepare**
once. It makes the maps from your own game files (it downloads about 200 MB of tools the first
time). In the game, choose **Map** in the quick menu (its fourth tile), press **M**, or with a
controller open the game's menu and click the right stick (R3). The port's menu (Insert, or hold
L3 + R3) changes the key and the controller shortcut.
**Travel to lamps from the map** and **Items on the map** are in the same launcher group.
## How do I get FSR 4.1.2 (INT8, for RDNA2 and the Steam Deck)?
FSR 4.1.2's models are built from AMD's own files, which can't be shipped here. In
**Graphics → Upscaler**, on **FSR 4.1.2 from AMD's driver**, press **Choose a file…** and pick
AMD's Adrenalin 26.10.1 or newer installer (the `.exe`) or the `amdxcffx64.dll` inside it. It also
needs AMD's FSR 4.1.x upscaler DLL (`amd_fidelityfx_upscaler_dx12.dll`, from OptiScaler or a game
with FSR 4.1): if you built FSR 4.1.1 before, it uses that one, otherwise it asks for it. It takes
about 4 minutes. In the game, the port's menu chooses the model (INT8 RDNA2 or INT8 Lite).
## How do I get a log on the Steam Deck?
+1 -1
View File
@@ -13,7 +13,7 @@ This also works when the game is started from Steam or from the Steam Deck's Gam
option on once in the launcher, and every session saves its log.
The log's first line says which version ran, and for a test build which one
(`bbport Linux 0.51-linux.18, build 626ec908 fix/issue1-lamp-blur 2026-10-29`).
(`bbport Linux 0.70-linux.1, build 74b1a308 HEAD 2026-11-03 (bbport 0.7)`).
## Sending it
+7
View File
@@ -337,6 +337,13 @@ add_executable(camera-matrices-test EXCLUDE_FROM_ALL ../tests/test_camera_matric
target_compile_options(camera-matrices-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(camera-matrices-test PRIVATE ..)
# When a mirror is made again over its valid blocks only (gpu/layer/layer_mirror_policy.h). Pure,
# no Vulkan device: the regression test for the default run of 0.7 that compacted nothing at all.
# From the SrShadowy fork.
add_executable(layer-mirror-policy-test EXCLUDE_FROM_ALL ../tests/test_layer_mirror_policy.cpp)
target_compile_options(layer-mirror-policy-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(layer-mirror-policy-test PRIVATE .)
add_executable(scene-resolution-test EXCLUDE_FROM_ALL ../tests/test_scene_resolution.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(scene-resolution-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
# The test's Vulkan-Hpp inline methods have a different set of platform extensions
+54 -4
View File
@@ -8535,7 +8535,31 @@ void TickBbscSpectate() {
LOG_INFO(Debug, "BBSC spectate: a partner said dead and then alive while we were down - "
"it reloaded after a wipe, the guest follows");
}
if (session_alive && LivingPartnerExists()) {
// Second wipe signal: the host's own position reports. It says nothing while it loads, so a
// report after a silence means it came out of a load while we were down, with the boss not
// killed: it reloaded after a wipe. The life packets above can miss this (the host was already
// dead when our death began, or it woke in another map), which left the guest spectating until
// the safety limit, with its world unloaded around it (BBSC issue #44).
BbscFollowMessage host_report{};
{
std::scoped_lock lock{bbsc_follow_mutex};
host_report = bbsc_follow_latest;
}
const bool host_report_fresh =
host_report.received_ms != 0 && now - host_report.received_ms < BbscFollowStaleMs;
const bool host_came_back = session_state == 6 && host_report_fresh &&
host_report.arrival_by_load &&
host_report.arrival_ms >= bbsc_spectate_since_ms &&
bbsc_boss_killed_ms < bbsc_spectate_since_ms &&
!BbscAllPartnersReportDead();
if (host_came_back && !bbsc_spectate_saw_wipe) {
bbsc_spectate_saw_wipe = true;
BBSC_TIMELINE("SPECTATE", "the host came out of a load in map {:#x} while we were down - "
"the guest follows",
host_report.packed_map);
}
// A host that woke in another map has no character here, so LivingPartnerExists can't see it.
if (session_alive && (LivingPartnerExists() || host_came_back)) {
std::string_view respawn_why;
const u64 chr = GetLocalPlayerCharacter();
if (bbsc_death_mode == BbscDeathMode::RespawnNow) {
@@ -8558,13 +8582,39 @@ void TickBbscSpectate() {
? real_host
: FindLivingPartnerCharacter();
const auto host_transform = ReadCharacterTransform(host);
if (host_transform.has_value()) {
SummonPlacementDescriptor to{};
to.packed_region = GetCurrentPackedMap();
const u32 here = GetCurrentPackedMap();
SummonPlacementDescriptor to{};
bool have_target = false;
if (host_report_fresh && host_report.packed_map != here) {
// The host's coordinates only mean something in its own map: load that map, with
// its world state, as a follow does.
to.packed_region = host_report.packed_map;
to.x = host_report.x + 1.0f;
to.y = host_report.y;
to.z = host_report.z;
to.heading = host_report.heading;
have_target = true;
if (BbscHostWorldFlagsReady(to.packed_region)) {
ApplyBbscHostWorldFlags(to.packed_region, "before the wipe load");
}
ApplyBbscHostTime("before the wipe load");
ApplyBbscGuestLootView(to.packed_region, "before the wipe load");
if (to.packed_region == BbscDreamStateMap) {
ApplyBbscHostDreamState("before the wipe load");
} else {
RestoreBbscOwnDreamState("following the host after the wipe");
}
} else if (host_transform.has_value()) {
to.packed_region = here;
to.x = host_transform->position[0] + 1.0f;
to.y = host_transform->position[1];
to.z = host_transform->position[2];
to.heading = host_transform->rotation[1];
have_target = true;
}
if (have_target) {
BBSC_TIMELINE("SPECTATE", "wipe: reloading beside the host in map {:#x}",
to.packed_region);
if (RequestBbscSessionWarp(GetGlobalState(), to, "wipe")) {
return; // the load builds a new character: "a load ended the boss fight"
}
+2 -1
View File
@@ -550,7 +550,8 @@ private:
void LayerProcessIdle();
/// VRAM near the texture collector's critical mark: idle mirrors go back in place.
void LayerYieldToImages(u32 now);
/// Makes mirrors with memory left without valid blocks again over those blocks only.
/// Makes mirrors with memory left without valid blocks again over those blocks only
/// (gpu/layer/layer_mirror_policy.h: the per-mirror threshold and the segments).
u64 LayerCompactMirrors(u64 wanted);
/// The GPU writes [address, address + size) in the submission being recorded (write ticks).
void NoteWriteTick(VAddr address, u64 size);
+12 -10
View File
@@ -664,12 +664,14 @@ void CommandProcessor::SubmitGfx(std::span<const u32> dcb, std::span<const u32>
BbFreeCheck::NoteSubmit(work->seq, work->copy ? work->copy->guest_dcb : dcb.data(),
dcb.size_bytes());
queue.works.push_back(std::move(work));
// Visible only once counted: the decoder reads this queue under the same lock,
// so the count it tests must be published before the task can be seen.
std::scoped_lock submit_lock{submit_mutex};
++num_submits;
++submissions_total;
BbTimeline::Note(BbTimeline::GuestSubmit, 0, submissions_total);
work_retired = false;
}
std::scoped_lock lock{submit_mutex};
++num_submits;
++submissions_total;
BbTimeline::Note(BbTimeline::GuestSubmit, 0, submissions_total);
work_retired = false;
submit_cv.notify_one();
}
@@ -684,12 +686,12 @@ void CommandProcessor::SubmitAsc(u32 gnm_vqid, std::span<const u32> acb) {
std::scoped_lock lock{queue.mutex};
work->stamp = BbStats::guest_submit_stamp.fetch_add(1, std::memory_order_acq_rel) + 1;
queue.works.push_back(std::move(work));
std::scoped_lock submit_lock{submit_mutex};
num_mapped_queues = std::max(num_mapped_queues, gnm_vqid + 1);
++num_submits;
++submissions_total;
work_retired = false;
}
std::scoped_lock lock{submit_mutex};
num_mapped_queues = std::max(num_mapped_queues, gnm_vqid + 1);
++num_submits;
++submissions_total;
work_retired = false;
submit_cv.notify_one();
}
+62 -16
View File
@@ -41,10 +41,14 @@
#include "bbgpu.h"
#include "bbport_guest_code_hooks.h"
#include "bbport_atlas_menu.h"
#include "bbport_overlay.h"
#include "bbport_toggles.h"
#include "common/types.h"
// bb-probe (runtime_services.c); weak: the GPU library's tests link without it.
extern "C" void runtime_quit_at_title(void) __attribute__((weak));
namespace BbMenuPointer {
namespace {
@@ -436,6 +440,12 @@ bool InputBlocked() {
return block && Get<std::uint32_t>(block + 0x2e4);
}
/// The game's title menu (the menu flow's +0xb11 zero), as InMenu reads it.
bool AtTitle() {
const Addr flow = Get<Addr>(At(MenuFlow));
return flow && !Get<std::uint8_t>(flow + 0xb11, 1);
}
bool InMenu() {
const Addr flow = Get<Addr>(At(MenuFlow));
const bool title = flow && !Get<std::uint8_t>(flow + 0xb11, 1);
@@ -520,24 +530,35 @@ void PS4_SYSV_ABI OnListUpdate(std::uint64_t, const Core::GuestRegisterSnapshot*
}
return;
}
static const bool trace = bb_env("BB_MENU_POINTER_FILE") != nullptr;
if ((!moved && !clicked) || pad_steering.load(std::memory_order_relaxed)) {
if (trace && clicked) {
std::printf("Menu pointer: list %p click waits (pad steering)\n",
static_cast<void*>(component));
}
return;
}
std::uint64_t slot = 0;
const int index = list.ItemAt(x, y, slot);
if (index < 0) {
if (trace && clicked) {
std::printf("Menu pointer: list %p click at %.0f,%.0f, on none of its items\n",
static_cast<void*>(component), x, y);
}
return;
}
if (clicked) {
click.store(false, std::memory_order_relaxed);
}
static const bool trace = bb_env("BB_MENU_POINTER_FILE") != nullptr;
if (trace) {
std::printf("Menu pointer: list %p item %d (cursor %d)%s\n", static_cast<void*>(component),
index, list.CursorIndex(), clicked ? " click" : "");
}
if (list.MoveTo(index, slot) && clicked) {
const std::uint32_t button = list.ClickButton(slot, x, y, ConfirmButton());
if (trace) {
std::printf("Menu pointer: list %p presses %#x\n", static_cast<void*>(component), button);
}
presses.fetch_or(button, std::memory_order_relaxed);
// A held click on a slider's arrow keeps moving it.
if (button == Left || button == Right) {
@@ -602,27 +623,41 @@ int OnEvent(const void* sdl_event, void* sdl_window) {
default:
break;
}
const bool ours = !bbgpu_mouse_captured() && !BbOverlay::MenuOpen() && InMenu();
// Not BbOverlay::MenuOpen(): that is also true while Hunter's Atlas's tile shows in the game's
// quick menu, and then no click reached the game's own items (only the tile's Map opened).
// A click on the tile itself is left to it (BbAtlasMenu::HandleMouse, after this filter),
// and so is its release, on which it opens the map.
const bool atlas_tile = BbAtlasMenu::Visible() && BbAtlasMenu::Hovered();
const bool ours =
!bbgpu_mouse_captured() && !BbOverlay::CapturesInput() && !atlas_tile && InMenu();
static const bool trace = bb_env("BB_MENU_POINTER_FILE") != nullptr;
if (trace && event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
std::printf("Menu pointer: button %d down (taken %d: captured %d, overlay %d, Atlas tile %d, "
"menu %d)\n",
int(event.button.button), ours ? 1 : 0, bbgpu_mouse_captured() ? 1 : 0,
BbOverlay::CapturesInput() ? 1 : 0, atlas_tile ? 1 : 0, InMenu() ? 1 : 0);
}
static bool left_taken = false; ///< the left button's press was the pointer's: so is its release
if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && event.button.button == SDL_BUTTON_LEFT) {
left_held.store(false, std::memory_order_relaxed);
repeat_press.store(0, std::memory_order_relaxed);
return ours ? 1 : 0;
const bool taken = left_taken;
left_taken = false;
return taken ? 1 : 0;
}
if (!ours) {
return 0;
}
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
// Only the left button is the pointer's. The others keep their bindings in a menu too
// (runtime_pad/sample.c), as the game's own prompts show them: the right button is L2, which
// switches the storage box's pages (#44); it used to be the way back, and with it nothing
// could press L2 in a menu. Escape is the way back.
if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT) {
SetPointer(window, event.button.x, event.button.y);
if (event.button.button == SDL_BUTTON_LEFT) {
left_held.store(true, std::memory_order_relaxed);
click_ms.store(NowMs(), std::memory_order_relaxed);
click.store(true, std::memory_order_relaxed);
} else if (event.button.button == SDL_BUTTON_RIGHT) {
presses.fetch_or(BackButton(), std::memory_order_relaxed);
}
return 1;
}
if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
left_held.store(true, std::memory_order_relaxed);
click_ms.store(NowMs(), std::memory_order_relaxed);
click.store(true, std::memory_order_relaxed);
left_taken = true;
return 1;
}
if (event.type == SDL_EVENT_MOUSE_WHEEL) {
@@ -636,7 +671,7 @@ int OnEvent(const void* sdl_event, void* sdl_window) {
return 0;
}
/// BB_MENU_POINTER_FILE=<file> (tests): "x y [click|right|up|down]" in stage pixels, applied
/// BB_MENU_POINTER_FILE=<file> (tests): "x y [click|back|up|down]" in stage pixels, applied
/// when the file changes, as the mouse would.
void ReadTestFile() {
static const char* path = bb_env("BB_MENU_POINTER_FILE");
@@ -668,7 +703,7 @@ void ReadTestFile() {
if (!std::strcmp(action, "click")) {
click_ms.store(now, std::memory_order_relaxed);
click.store(true, std::memory_order_relaxed);
} else if (!std::strcmp(action, "right")) {
} else if (!std::strcmp(action, "back")) {
presses.fetch_or(BackButton(), std::memory_order_relaxed);
} else if (!std::strcmp(action, "up") || !std::strcmp(action, "down")) {
presses.fetch_or(action[0] == 'u' ? Up : Down, std::memory_order_relaxed);
@@ -679,6 +714,17 @@ void ReadTestFile() {
/// The pad state the game reads (pad thread): the pointer's presses, each held long enough for
/// the game to see it, with a gap after.
void OnPad(std::uint32_t* buttons, const std::uint8_t* sticks) {
// Exit to desktop (runtime_services.c): the game going from the world to its title menu is the
// title it waits for. Issue #46: with online play on, the game came back to the title without
// opening the intro logos or looking its title menu's texts up again, and closed only when a
// new game was started from there. Only the change counts (the flag is also set at start);
// runtime_quit_at_title does nothing unless Exit to desktop is armed.
static bool was_title = true;
const bool title = AtTitle();
if (title && !was_title && runtime_quit_at_title) {
runtime_quit_at_title();
}
was_title = title;
const auto off_centre = [](std::uint8_t v) { return v < 64 || v > 192; };
pad_steering.store((*buttons & (Up | Right | Down | Left)) || off_centre(sticks[0]) ||
off_centre(sticks[1]),
+2 -2
View File
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Bloodborne (1.09): the mouse as a pointer in the game's own menus (Scaleform lists: the title
// menu, the inventory, the shops, the options). The item under the pointer gets the game's cursor,
// a click presses the confirm button on it (or the slider's arrow it is on), the right button goes
// back, the wheel moves up and down. bloodborne_menu_pointer.cpp has the reverse engineering.
// a click presses the confirm button on it (or the slider's arrow it is on), the other buttons keep
// their bindings (the right one L2), the wheel moves up and down. bloodborne_menu_pointer.cpp has the reverse engineering.
#pragma once
namespace BbMenuPointer {
+87 -30
View File
@@ -44,6 +44,7 @@
#include "bblayer_gpu_memory.h"
#include "bblayer_write_traps.h"
#include "layer/buffer_cache_layer_internal.h"
#include "layer/layer_mirror_policy.h"
namespace VideoCore {
@@ -347,6 +348,7 @@ bool BufferCache::LayerMergeAcross(u64 first, u64 end) {
BbStats::residency_alloc_bytes.fetch_sub(old.size, std::memory_order_relaxed);
layer_retired.push_back({std::move(node.mapped()), scheduler.CurrentTick()});
}
layer_mirror_born.erase(old.base); // it is gone: its start may be reused by another mirror
}
Layer().AddMirror(start, bytes, merged->Handle(), merged->BufferDeviceAddress(), merged.get());
layer_mirrors.emplace(start, std::move(merged));
@@ -416,6 +418,7 @@ u64 BufferCache::LayerPromoteInto(u64 first, u64 end, u64 lo, u64 hi,
BbStats::residency_alloc_bytes.fetch_sub(old.size, std::memory_order_relaxed);
layer_retired.push_back({std::move(node.mapped()), scheduler.CurrentTick()});
}
layer_mirror_born.erase(old.base); // it is gone: its start may be reused by another mirror
}
Layer().AddMirror(start, bytes, target->Handle(), target->BufferDeviceAddress(), target);
layer_mirrors.emplace(start, std::move(mirror));
@@ -555,6 +558,7 @@ void BufferCache::LayerDemote(u64 first, u64 end, bool copy_back, int reason) {
BbStats::residency_alloc_bytes.fetch_sub(mirror.size, std::memory_order_relaxed);
layer_retired.push_back({std::move(node.mapped()), scheduler.CurrentTick()});
}
layer_mirror_born.erase(mirror.base); // it is gone: its start may be reused by another mirror
}
++layer_generation;
}
@@ -637,10 +641,22 @@ std::unique_ptr<Buffer> BufferCache::LayerNewMirror(VAddr start, u64 bytes) {
}
++mirror_pool_misses;
}
// BB_MIRROR_VRAM_ONLY=1: VRAM or none (VMA puts "mirrors" in system memory when VRAM is full).
// Off by default: VMA's budget check refused them at 1.5-1.65 GB of a 1.77-1.83 GB budget on
// the 2 GB laptop, the geometry was read over the bus and the game fell to 8-29 FPS (2026-10-26).
static const bool system_ok = !bb_env_on("BB_MIRROR_VRAM_ONLY");
// VRAM or none: without it VMA puts a mirror in system memory when VRAM is full, and there it
// stays until its blocks are retired, so the GPU reads that geometry over the bus until a
// reload (#42: an RTX 4060 at 17-27 FPS in the same places; BB_MIRROR_VRAM_ONLY=1 fixed it).
// A mirror refused instead is made again later, when there is room. On by default on cards
// with more than 4 GiB; small cards keep the fallback: VMA's budget check refused mirrors at
// 1.5-1.65 GB of a 1.77-1.83 GB budget on the 2 GB laptop, the geometry was read over the bus
// and the game fell to 8-29 FPS (2026-10-26). BB_MIRROR_VRAM_ONLY=1 / 0 forces either.
static const bool system_ok = [this] {
const bool vram_only = bb_env_on("BB_MIRROR_VRAM_ONLY") ? true
: bb_env_off("BB_MIRROR_VRAM_ONLY") ? false
: instance.GetTotalMemoryBudget() > (u64{4} << 30);
std::printf("Layer memory: mirrors %s (BB_MIRROR_VRAM_ONLY)\n",
vram_only ? "in VRAM only, made again later when it is full"
: "in system memory too when VRAM is full");
return !vram_only;
}();
std::optional<VramOnlyScope> vram_only;
if (!system_ok) {
vram_only.emplace();
@@ -756,11 +772,15 @@ void BufferCache::LayerMaintain() {
return u32(std::max<long>(env && *env ? std::strtol(env, nullptr, 10) : 1, 1));
}();
static u32 compacted_second = 0;
// Waste no compaction could free (valid blocks closer than its Bridge): not tried again until
// more of it comes (the mirrors were made again every second, 0 MiB freed).
// Waste no compaction could free last time: it is not tried again until more of it comes,
// or kRetrySeconds pass. The time matters because the mirrors that were too young (or whose
// waste sits between their valid blocks) become eligible as they grow older and lose blocks;
// without it a run with nothing to compact rescanned every mirror every second for nothing.
static u64 stuck_waste = 0;
static u32 retry_after = 0;
if (const u32 second = BbStats::coarse_second.load(std::memory_order_relaxed);
compact_at != 0 && second - compacted_second >= compact_every) {
const bool retry_due = second >= retry_after;
compacted_second = second;
u64 waste = 0;
for (const auto& [start, mirror] : layer_mirrors) {
@@ -769,11 +789,23 @@ void BufferCache::LayerMaintain() {
[&](u64 a, u64 b) { valid += b - a; });
waste += mirror->SizeBytes() - (valid << block_shift);
}
// The floor only goes down with the waste: the growth is what justifies another look.
if (waste < stuck_waste) {
stuck_waste = waste;
}
if (waste >= compact_at && waste >= stuck_waste + compact_at / 2) {
stuck_waste = LayerCompactMirrors(waste) == 0 ? waste : 0;
// Either more waste came (compact_at / 2 of it), or the retry is due: the same waste
// seen again later may now be in mirrors old enough to be made again.
if (waste >= compact_at &&
(retry_due || waste >= stuck_waste + compact_at / 2)) {
// One call takes a bounded share of the waste: the copies it costs then spread over
// the seconds that follow instead of landing in one long frame.
if (LayerCompactMirrors(LayerPolicy::CompactBudget(waste)) == 0) {
stuck_waste = waste;
retry_after = second + LayerPolicy::kRetrySeconds;
} else {
stuck_waste = 0;
retry_after = 0;
}
}
}
// Retired mirrors the GPU is done with. BB_LAYER_CHECK=1: no page table entry may still point
@@ -1106,10 +1138,11 @@ void BufferCache::NoteDirectUse(VAddr address, u64 size) {
u64 BufferCache::LayerCompactMirrors(u64 wanted) {
// Mirror memory without valid blocks (sent back in place, unmapped, handed out anew, the room
// to grow) stays allocated as long as its mirror lives. Mirrors with at least MinWaste of it,
// the most first, are made again over their valid blocks only: one new mirror per run of
// valid blocks (runs closer than Bridge stay in one), their data copied over, the old mirror
// retired as in a merge. No copy is lost.
// to grow) stays allocated as long as its mirror lives. Mirrors with enough of it, the most
// first, are made again over their valid blocks only: one new mirror per run of valid blocks
// (the heads and tails always come off; a hole between two runs pays for the mirror it needs,
// see LayerPolicy::MirrorSegments), their data copied over, the old mirror retired as in a
// merge. No copy is lost.
// Mirrors younger than BB_MIRROR_COMPACT_AGE seconds (30) are left alone unless VRAM is past the
// texture collector's critical mark: on the 2 GB laptop compaction cut the room a mirror had
// just grown, the next run grew it again (80, 52, 112, 64 MiB within seconds), 411 mirrors of
@@ -1122,19 +1155,30 @@ u64 BufferCache::LayerCompactMirrors(u64 wanted) {
const u64 critical = BbStats::gc_critical_bytes.load(std::memory_order_relaxed);
const bool emergency =
critical != 0 && BbStats::gc_used_bytes.load(std::memory_order_relaxed) >= critical;
constexpr u64 MinWaste = 8_MB, Bridge = 4_MB;
std::vector<std::pair<u64, VAddr>> wasteful; // bytes without valid blocks, start
// Why a pass could not act, for the log: a default run that compacts nothing has to say which
// filter stopped it (the age gate, the per-mirror threshold, waste between valid blocks, no
// room), or every acceptance run ends in the same guess.
size_t young = 0, below = 0, whole = 0, noroom = 0;
u32 oldest_young = 0; // the age of the mirror the age gate held back the longest
for (const auto& [start, mirror] : layer_mirrors) {
if (const auto born = layer_mirror_born.find(start);
!emergency && born != layer_mirror_born.end() && now - born->second < min_age) {
++young;
oldest_young = std::max(oldest_young, now - born->second);
continue;
}
const u64 lo = start >> block_shift, hi = (start + mirror->SizeBytes()) >> block_shift;
const u64 size = mirror->SizeBytes();
const u64 lo = start >> block_shift, hi = (start + size) >> block_shift;
u64 valid = 0;
Layer().ForEachValid(lo, hi, [&](u64 a, u64 b) { valid += b - a; });
const u64 waste = mirror->SizeBytes() - (valid << block_shift);
if (waste >= MinWaste) {
const u64 waste = size - (valid << block_shift);
// The threshold follows the mirror's size: a fixed 8 MiB was more than a whole mirror of
// the default run (7.8 MiB), so nothing was ever compacted there.
if (waste >= LayerPolicy::MirrorMinWaste(size)) {
wasteful.emplace_back(waste, start);
} else {
++below;
}
}
std::ranges::sort(wasteful, std::greater{});
@@ -1146,20 +1190,19 @@ u64 BufferCache::LayerCompactMirrors(u64 wanted) {
}
const u64 old_size = layer_mirrors.at(start)->SizeBytes();
const u64 lo = start >> block_shift, hi = (start + old_size) >> block_shift;
std::vector<std::pair<u64, u64>> segments;
u64 remade = 0;
Layer().ForEachValid(lo, hi, [&](u64 a, u64 b) {
if (!segments.empty() && ((a - segments.back().second) << block_shift) < Bridge) {
segments.back().second = b;
} else {
segments.emplace_back(a, b);
}
});
for (const auto& [a, b] : segments) {
remade += (b - a) << block_shift;
}
if (old_size - remade < MinWaste) {
continue; // its waste lies between valid blocks: copies for nothing
std::vector<LayerPolicy::Segment> runs;
Layer().ForEachValid(lo, hi, [&](u64 a, u64 b) { runs.push_back({a, b}); });
const auto segments = LayerPolicy::MirrorSegments(
runs, block_shift, LayerPolicy::MirrorMinHole(old_size),
LayerPolicy::kMaxSegmentsPerMirror);
const u64 remade = LayerPolicy::SegmentsBytes(segments, block_shift);
// What it would actually free. The per-mirror threshold is asked again here: a mirror
// whose waste lies between its valid blocks comes back whole (the head and the tail are
// all it can drop), and copying it for a few KiB is the failure of 2026-10-25 (411
// mirrors in 8 minutes for 0.4 GB of data on the 2 GB laptop).
if (old_size - remade < LayerPolicy::MirrorMinWaste(old_size)) {
++whole;
continue;
}
// The new mirrors first: without room in VRAM for all of them this one stays as it is.
std::vector<std::unique_ptr<Buffer>> made;
@@ -1173,6 +1216,7 @@ u64 BufferCache::LayerCompactMirrors(u64 wanted) {
made.push_back(std::move(mirror));
}
if (made.size() != segments.size()) {
++noroom;
continue;
}
auto node = layer_mirrors.extract(start);
@@ -1213,6 +1257,19 @@ u64 BufferCache::LayerCompactMirrors(u64 wanted) {
std::printf("Layer memory: %zu mirrors made again over their valid blocks, "
"%llu MiB freed\n",
compacted, (unsigned long long)(freed >> 20));
} else if (!layer_mirrors.empty()) {
// Nothing came back, and the run needs to say why instead of leaving it to the next
// acceptance run to guess: too young (the age gate), under the per-mirror threshold, waste
// all between valid blocks (it would come back whole), or no room for the new mirrors.
// Rate limited: the VRAM pressure path can call this every frame while it is short.
static u32 printed = 0; // coarse seconds; 0 = not yet
if (printed == 0 || now - printed >= 10) {
printed = now;
std::printf("Layer memory: nothing compacted: %zu young (age gate %u s, oldest %u s), "
"%zu under the per-mirror waste, %zu waste between valid blocks, %zu without "
"room\n",
young, min_age, oldest_young, below, whole, noroom);
}
}
return freed;
}
+130
View File
@@ -0,0 +1,130 @@
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: when a mirror is worth making again over its valid blocks only (BufferCache::
// LayerCompactMirrors), and from which of its valid runs. Pure: no Vulkan, no guest, no
// environment, so the tests cover it without a device (tests/test_layer_mirror_policy.cpp).
#pragma once
#include <algorithm>
#include <cstddef>
#include <vector>
#include "common/types.h"
namespace VideoCore::LayerPolicy {
/// A run of blocks: [first, end), both in the layer's blocks.
struct Segment {
u64 first = 0;
u64 end = 0;
bool operator==(const Segment&) const = default;
};
/// The waste (bytes without valid blocks) a mirror of `bytes` must have before it is made again:
/// a quarter of it, between 1 and 8 MiB. A mirror of 4 MiB therefore needs 1 MiB of waste, one of
/// 8 MiB needs 2 MiB, and from 32 MiB up it is 8 MiB.
///
/// The fixed 8 MiB of 0.7 was the size of a whole mirror on the run it was chosen for: 223-225
/// mirrors of 1,750 MiB (7.8 MiB each) held 612 MiB of waste between them (2.7 MiB each), so not
/// one of them ever reached it and nothing was compacted in the whole run (default run without any
/// BB_MIRROR_* variable, 2026-11-05).
inline u64 MirrorMinWaste(u64 bytes) {
return std::clamp<u64>(bytes / 4, 1_MB, 8_MB);
}
/// A hole (bytes without valid blocks between two valid runs of a mirror) is only split out into
/// a mirror of its own when it is worth one: a thirty-second of the mirror, between 256 KiB and
/// 4 MiB. Below that the copy into the extra mirror costs more than the hole frees -- the run of
/// 2026-10-25 made 411 mirrors in 8 minutes for 0.4 GB of data that way.
inline u64 MirrorMinHole(u64 bytes) {
return std::clamp<u64>(bytes / 32, 256_KB, 4_MB);
}
/// The most mirrors one compacted mirror becomes. The holes that have to be kept to stay under it
/// are the smallest ones: a mirror split into a hundred pieces costs more to bind than its waste.
constexpr u32 kMaxSegmentsPerMirror = 8;
/// The waste one extra mirror has to free to be worth making: below it the split costs (a copy of
/// the data and one more binding range) more than it gives back.
constexpr u64 kSegmentPays = 1_MB;
/// The segments a mirror is made again from, given its valid runs.
///
/// `runs` must be sorted and must not overlap. The heads and tails (the waste before the first run
/// and after the last) are always dropped: that costs no extra mirror, only the copy it already
/// makes. A hole between two runs is split out when it is at least `min_hole`, while the pieces
/// stay under `max_segments`, and only when the waste freed pays for the extra mirrors
/// (`kSegmentPays` each). So a mirror whose waste is scattered in small holes between its data
/// comes back as one segment and the caller sees no gain -- which is the right answer: it would
/// have been copies for nothing.
///
/// The smallest holes are the ones kept when a limit is reached.
inline std::vector<Segment> MirrorSegments(const std::vector<Segment>& runs, u64 shift,
u64 min_hole, u32 max_segments) {
std::vector<Segment> segments;
if (runs.empty()) {
return segments;
}
// The head and the tail are dropped for free: they come off without an extra mirror.
const std::size_t holes = runs.size() - 1;
const auto gap = [&](std::size_t i) {
return (runs[i + 1].first - runs[i].end) << shift;
};
const u32 limit = std::max<u32>(max_segments, 1);
// Holes worth a mirror of their own, smallest first when some have to go back.
std::vector<std::size_t> candidates;
for (std::size_t i = 0; i < holes; ++i) {
if (gap(i) >= min_hole) {
candidates.push_back(i);
}
}
std::ranges::sort(candidates, {}, gap);
// The biggest holes first, each as long as the waste it frees pays for the extra mirror it
// costs (kSegmentPays each). The head and the tail are free: they come off without one.
std::vector<bool> split(holes, false);
u64 freed = 0;
u32 pieces = 1;
for (auto it = candidates.rbegin(); it != candidates.rend(); ++it) {
const u32 next = pieces + 1;
if (next > limit || freed + gap(*it) < u64(next - 1) * kSegmentPays) {
break; // sorted biggest first: nothing after this one pays either
}
split[*it] = true;
freed += gap(*it);
pieces = next;
}
Segment current{runs.front().first, runs.front().end};
for (std::size_t i = 0; i < holes; ++i) {
if (split[i]) {
segments.push_back(current);
current = Segment{runs[i + 1].first, runs[i + 1].end};
} else {
current.end = runs[i + 1].end;
}
}
segments.push_back(current);
return segments;
}
/// The bytes the segments hold, to compare with the mirror's size.
inline u64 SegmentsBytes(const std::vector<Segment>& segments, u64 shift) {
u64 bytes = 0;
for (const auto& segment : segments) {
bytes += (segment.end - segment.first) << shift;
}
return bytes;
}
/// After a compaction that freed nothing, the next attempt waits this long: the mirrors it looked
/// at could not be made again (their waste all lies between valid blocks, or they are too young),
/// and rescanning them every second changes nothing while costing the whole scan.
constexpr u32 kRetrySeconds = 30;
/// The waste one compaction call takes, so the copies it costs are spread over the seconds that
/// follow instead of landing in one long frame. The pressure path passes its own need instead.
inline u64 CompactBudget(u64 waste) {
return std::min<u64>(waste, 32_MB);
}
} // namespace VideoCore::LayerPolicy
+3 -2
View File
@@ -106,7 +106,7 @@ BB_ENV(BB_SHADNET_PASSWORD, SETTING, "the shadNet account password (the launcher
BB_ENV(BB_SHADNET_SERVER, SETTING, "the shadNet server")
BB_ENV(BB_SHADNET_TOKEN, SETTING, "the shadNet account token (the launcher's)")
BB_ENV(BB_SHADNET_WEBAPI, SETTING, "the shadNet web API server")
BB_ENV(BB_SKIP_CPU_CHECK, SETTING, "=1: skip the check of the CPU's bsr/bsf behaviour before the game starts")
BB_ENV(BB_SKIP_CPU_CHECK, SETTING, "=1: skip the check of the processor's instructions (AVX, BMI1, LZCNT...) before the game starts")
BB_ENV(BB_SKIP_GAME_CHECK, SETTING, "=1: start with game files that did not pass the check")
BB_ENV(BB_STEAM_INPUT_X11, SETTING, "=0: keep the native Wayland window when Steam made a virtual controller for the game (default: Xwayland, so Steam Input reaches it)")
BB_ENV(BB_TEXTURE_QUALITY, SETTING, "texture quality over bbport.ini's texture_quality")
@@ -261,7 +261,7 @@ BB_ENV(BB_MIRROR_MERGE_GROWTH, TUNING, "=0: a mirror made over neighbours grows
BB_ENV(BB_MIRROR_POOL_MB, TUNING, "=N: retired mirrors kept for new ones")
BB_ENV(BB_MIRROR_REPROMOTE_SECONDS, TUNING, "=N (default 0): blocks sent back are not copied again that soon")
BB_ENV(BB_MIRROR_UNBOUND_SECONDS, TUNING, "=N (0 never): blocks no binding named for N seconds leave VRAM")
BB_ENV(BB_MIRROR_VRAM_ONLY, TUNING, "=1: mirrors in VRAM or not at all")
BB_ENV(BB_MIRROR_VRAM_ONLY, TUNING, "=1 / 0: mirrors in VRAM or not at all, or in system memory too when VRAM is full (default: VRAM only on cards over 4 GiB)")
BB_ENV(BB_MOTION_LARGE, TUNING, "=0: bone palettes of 16-64 KiB (the large beasts) get camera motion vectors only, not object motion")
BB_ENV(BB_MOTION_VERTICES_MI, TUNING, "=N (1-8): object motion vertex buffer in Mi vertices")
BB_ENV(BB_NET_MODULE, TUNING, "=path: the online module to load (default gpu/libbbnet20.so beside bb-probe)")
@@ -410,6 +410,7 @@ BB_ENV(BB_EXP_COPY_SRC_SNAPSHOT, DEBUG, "set (experiment): copy shader sources s
BB_ENV(BB_EXP_CPU_READS_GPU_WAIT, DEBUG, "set (experiment): CPU-side reads also wait for the GPU, as before")
BB_ENV(BB_EXP_DMA_UNTRACKED, DEBUG, "set (experiment): stages reading memory through pointers count as unlisted reads")
BB_ENV(BB_FAULT_WAIT, DEBUG, "=1: a fatal fault or a GPU library assertion leaves the process alive for a debugger (tools/debug_attach.sh)")
BB_ENV(BB_MOVBE_TRAP, DEBUG, "=file: the movbe at the image offsets listed there (tools/movbe_sites.py) made ud2 and run by the port, as on a processor without MOVBE")
BB_ENV(BB_WRITE_SHADOWS, DEBUG, "=1: ranges the game's copy shader writes in place copied into VRAM after it, small read-only bindings read there (no gain measured)")
BB_ENV(BB_WRITE_SHADOW_TRACE, DEBUG, "=1: why BB_WRITE_SHADOWS lookups miss and who drops the copies (log lines)")
BB_ENV(BB_HOT_VERTEX_COPY, TUNING, "=1: vertex data bound in place copied into VRAM once per submission (no reads over the bus per pass)")
+1
View File
@@ -71,6 +71,7 @@ bool Interacting(){return visible || BbMap::IsOpen() || runtime_map_menu_open();
bool UsesControllerGlyphs(){const char* qa=bb_env("BB_ATLAS_QA_DEVICE");return bb_env("BB_ATLAS_QA")&&qa?std::strcmp(qa,"controller")==0:bool(controller);}
bool UsesPlayStationGlyphs(){const auto type=SDL_GetGamepadTypeForID(last_controller.load());return type==SDL_GAMEPAD_TYPE_PS3||type==SDL_GAMEPAD_TYPE_PS4||type==SDL_GAMEPAD_TYPE_PS5;}
bool Focused(){return focused;}
bool Hovered(){return hovered;}
void Suppress(bool value){suppressed=value;if(value){visible=false;state.Reset();}}
void Reset(){state.Reset();focused=false;visible=false;hovered=false;mouse_held=false;map_previous=0;}
void Tick(){Refresh();if(BbMap::IsOpen()&&!runtime_map_menu_open()&&state.focused){BbMap::Close();Reset();}}
+2
View File
@@ -8,6 +8,8 @@ bool Interacting();
bool UsesControllerGlyphs();
bool UsesPlayStationGlyphs();
bool Focused();
/// The pointer is over the tile (the game's own menu pointer leaves its clicks to it).
bool Hovered();
void Suppress(bool value);
void Tick();
void Draw();
+76 -3
View File
@@ -26,6 +26,10 @@
#include "bbport_settings.h"
#include <fmt/format.h>
// bb-probe (runtime_services.c); weak: the GPU library's tests link without it.
extern "C" void runtime_quit_arm(int on) __attribute__((weak));
extern "C" void runtime_quit_at_title(void) __attribute__((weak));
namespace BbGameMenu {
namespace {
@@ -78,11 +82,23 @@ struct Layout {
};
constexpr Layout ControlsLayout{0x4934065, 113020, 0, 6}; // "ControllSetting"
constexpr u32 TextName = 0xc8, TextHelp = 0xc9; // SP_menu text / SP_one-line help
constexpr u32 TextDialog = 0xcc; // dialog texts
constexpr u32 ScreenSoundItem = 110001; // "Screen/Sound" in the System menu: ours follow it
/// The System menu's builder: (out, arg, in the world). Out of the world it drops Exit Game.
constexpr u64 SystemMenu = 0x1bb3ad0;
/// (menu, name+help texts, {vtable, fn} function, 0): an item that runs `fn` like a page factory
/// (Exit Game's, 0x1bb436d).
constexpr u64 AddActionItem = 0x1b4c9a0;
/// Exit Game's (out, arg): its question (dialog text ExitQuestion), then a save and the title.
constexpr u64 ExitGame = 0x1c0c9f0;
constexpr u32 ExitQuestion = 110000;
/// The title menu's items (0x1b4a4aa...): built when the game is back at the title.
constexpr u32 TitleItemFirst = 401301, TitleItemLast = 401307;
const u8 MsgLookupPrologue[] = {0x89, 0xf0, 0x48, 0x8b, 0x77, 0x08, 0x48, 0x8b, 0x34, 0xc6};
const u8 AddPageItemPrologue[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57};
const u8 RowInputPrologue[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57};
const u8 SystemMenuPrologue[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57};
u64 image_base = 0;
template <typename F>
@@ -337,7 +353,9 @@ struct Page {
std::vector<Page> pages;
// "Exit to desktop" in the System menu (the title screen's too): a Quit Game to the desktop, as PC
// games have (Discord requests 2026-10-21; the Nexus mod "Just Exit Game" did it until now).
u32 exit_name = 0, exit_help = 0;
// In the world it is the game's Exit Game first, so the game saves (runtime_services.c closes it
// at the title), with its question as ours: closing at once lost what was not saved yet (#25).
u32 exit_name = 0, exit_help = 0, exit_help_world = 0, exit_question = 0;
void DefineTexts() {
const u32 off = AddText("Off", "Выкл"), on = AddText("On", "Вкл");
@@ -457,6 +475,8 @@ void DefineTexts() {
exit_name = AddText("Exit to desktop", "Выйти на рабочий стол");
exit_help = AddText("Closes the game, as its window's close button does",
"Закрывает игру, как кнопка закрытия окна");
exit_help_world = AddText("Saves and closes the game", "Сохраняет и закрывает игру");
exit_question = AddText("Quit the game and return to the desktop?", "Выйти из игры на рабочий стол?");
}
// ---- The game's structures, as its own code builds them. ----
@@ -650,6 +670,19 @@ void* PS4_SYSV_ABI QuitFactory(void* out, void* arg) {
SDL_PushEvent(&event);
return Factory(out, arg, reinterpret_cast<void*>(&Content<0>), 0);
}
/// In the world: the game's Exit Game, with its question as ours, and the quit armed.
thread_local bool asking_exit = false;
void* PS4_SYSV_ABI SaveQuitFactory(void* out, void* arg) {
std::printf("Game menu: exit to desktop, through Exit Game (it saves)\n");
if (runtime_quit_arm) {
runtime_quit_arm(1);
}
using Factory = void* (PS4_SYSV_ABI*)(void*, void*);
asking_exit = true;
void* result = Game<Factory>(ExitGame)(out, arg);
asking_exit = false;
return result;
}
constexpr std::size_t MaxPages = 6;
void* const factories[MaxPages] = {
reinterpret_cast<void*>(&PageFactory<0>), reinterpret_cast<void*>(&PageFactory<1>),
@@ -675,6 +708,17 @@ AddFn add_original = nullptr;
std::atomic<const void*> screen_sound_text{nullptr};
const char16_t* PS4_SYSV_ABI LookupHook(void* repository, u32 table, u32 category, u32 id) {
if (category == TextDialog && id == ExitQuestion) {
if (asking_exit && exit_question) {
return TextOf(exit_question);
}
if (runtime_quit_arm) {
runtime_quit_arm(0); // the game's own Exit Game: back to the title only
}
}
if (category == TextName && id >= TitleItemFirst && id <= TitleItemLast && runtime_quit_at_title) {
runtime_quit_at_title();
}
if (OurText(id)) {
// BB_GAME_MENU_TRACE=1: how often the game asks for our texts (per 2 s).
static const bool count = bb_env_on("BB_GAME_MENU_TRACE");
@@ -766,6 +810,31 @@ void AddItem(void* menu, u32 name, u32 help, void* factory) {
DestroyTexts(pair);
}
/// An item like Exit Game's (AddActionItem): `factory` runs when it is chosen, its result shown
/// as the game shows Exit Game's question.
void AddActionEntry(void* menu, u32 name, u32 help, void* factory) {
alignas(16) u8 pair[2 * TextSize] = {};
MakeTexts(pair, name, help);
alignas(16) u8 function[FunctionSize] = {};
*reinterpret_cast<u64*>(function) = image_base + FactoryVtable;
*reinterpret_cast<void**>(function + 8) = factory;
*reinterpret_cast<void**>(function + 0x20) = function;
Game<AddFn>(AddActionItem)(menu, pair, function, nullptr);
DestroyFunction(function);
DestroyTexts(pair);
}
/// The System menu being built is the world's (it has Exit Game).
bool building_in_world = false;
using SystemMenuFn = void* (PS4_SYSV_ABI*)(void*, void*, u64);
SystemMenuFn system_menu_original = nullptr;
void* PS4_SYSV_ABI SystemMenuHook(void* out, void* arg, u64 in_world) {
building_in_world = (in_world & 0xff) != 0;
void* result = system_menu_original(out, arg, in_world);
building_in_world = false;
return result;
}
void* PS4_SYSV_ABI AddHook(void* menu, void* pair, void* function, void* out) {
void* result = add_original(menu, pair, function, out);
const void* screen_sound = screen_sound_text.load();
@@ -773,7 +842,9 @@ void* PS4_SYSV_ABI AddHook(void* menu, void* pair, void* function, void* out) {
for (std::size_t i = 0; i < pages.size() && i < MaxPages; ++i) {
AddItem(menu, pages[i].name, pages[i].help, factories[i]);
}
if (exit_name) {
if (exit_name && building_in_world) {
AddActionEntry(menu, exit_name, exit_help_world, reinterpret_cast<void*>(&SaveQuitFactory));
} else if (exit_name) {
AddItem(menu, exit_name, exit_help, reinterpret_cast<void*>(&QuitFactory));
}
}
@@ -858,7 +929,9 @@ void PatchImage(unsigned char* image, std::uint64_t size) {
Detour(image, AddPageItem, AddPageItemPrologue, sizeof(AddPageItemPrologue),
reinterpret_cast<void*>(&AddHook), reinterpret_cast<void**>(&add_original), cursor) &&
Detour(image, RowInput, RowInputPrologue, sizeof(RowInputPrologue),
reinterpret_cast<void*>(&RowInputHook), reinterpret_cast<void**>(&row_input_original), cursor);
reinterpret_cast<void*>(&RowInputHook), reinterpret_cast<void**>(&row_input_original), cursor) &&
Detour(image, SystemMenu, SystemMenuPrologue, sizeof(SystemMenuPrologue),
reinterpret_cast<void*>(&SystemMenuHook), reinterpret_cast<void**>(&system_menu_original), cursor);
mprotect(stubs, page, PROT_READ | PROT_EXEC);
std::printf("Game menu: %s (%zu pages in System)\n", ok ? "the port's pages are in" : "hooks failed, off",
pages.size());
+15 -9
View File
@@ -124,16 +124,22 @@ void FreeChunk(u64 phys, u64 size) {
std::uint32_t FindHostType(std::uint32_t bits) {
// Cached host memory first, then any host-visible type the import allows.
for (const bool cached : {true, false}) {
for (std::uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i) {
const auto flags = memory_properties.memoryTypes[i].propertyFlags;
if (!(bits & (1u << i)) || !(flags & vk::MemoryPropertyFlagBits::eHostVisible) ||
(flags & vk::MemoryPropertyFlagBits::eDeviceLocal) || !TypeAllowed(flags) ||
(cached &&!(flags & vk::MemoryPropertyFlagBits::eHostCached))) {
continue;
// Cached host memory first, then any host-visible type the import allows. System-memory types
// (not device-local) come first; a GPU with one memory pool (Intel ANV on integrated Arc/Xe:
// every type is device-local, the imports allow only those) then takes a device-local
// host-visible type: the pages stay the CPU's own, cached, memory.
for (const bool device_local : {false, true}) {
for (const bool cached : {true, false}) {
for (std::uint32_t i = 0; i < memory_properties.memoryTypeCount; ++i) {
const auto flags = memory_properties.memoryTypes[i].propertyFlags;
if (!(bits & (1u << i)) || !(flags & vk::MemoryPropertyFlagBits::eHostVisible) ||
bool(flags & vk::MemoryPropertyFlagBits::eDeviceLocal) != device_local ||
!TypeAllowed(flags) ||
(cached && !(flags & vk::MemoryPropertyFlagBits::eHostCached))) {
continue;
}
return i;
}
return i;
}
}
return ~0u;
+28 -7
View File
@@ -115,6 +115,20 @@ inline const char* MenuTranslation(MenuLanguage language, const char* english) {
"Schließt das Spiel wie die Schließen-Schaltfläche seines Fensters",
"Fecha o jogo, como o botão de fechar da janela",
"关闭游戏,与窗口的关闭按钮相同"}},
{"Saves and closes the game",
{"Guarda y cierra el juego",
"Salva e chiude il gioco",
"Sauvegarde et ferme le jeu",
"Speichert und schließt das Spiel",
"Salva e fecha o jogo",
"保存并关闭游戏"}},
{"Quit the game and return to the desktop?",
{"¿Salir del juego y volver al escritorio?",
"Uscire dal gioco e tornare al desktop?",
"Quitter le jeu et revenir au bureau ?",
"Spiel beenden und zum Desktop zurückkehren?",
"Sair do jogo e voltar para a área de trabalho?",
"要退出游戏并返回桌面吗?"}},
{"Bloom",
{"Bloom",
"Bloom",
@@ -745,6 +759,13 @@ inline const char* MenuTranslation(MenuLanguage language, const char* english) {
"Ragdoll-Physik wie in Dark Souls",
"Física ragdoll de Dark Souls",
"黑暗之魂的布娃娃物理"}},
{"L3 jumps while running",
{"L3 salta al correr",
"L3 salta durante la corsa",
"L3 saute en courant",
"L3 springt beim Rennen",
"L3 pula ao correr",
"奔跑时按 L3 跳跃"}},
// The game's System menu pages (bbport_game_menu.cpp).
{"Display",
{"Imagen",
@@ -1055,13 +1076,13 @@ inline const char* MenuTranslation(MenuLanguage language, const char* english) {
"%.0f FPS %.1f ms",
"%.0f FPS %.1f ms",
"%.0f FPS %.1f 毫秒"}},
{" VRAM -%u MB (other apps)",
{" VRAM -%u MB (otras aplicaciones)",
" VRAM -%u MB (altre app)",
" VRAM -%u Mo (autres applications)",
" VRAM -%u MB (andere Programme)",
" VRAM -%u MB (outros apps)",
" VRAM -%u MB(其他程序)"}},
{" VRAM: %u MB taken by other apps",
{" VRAM: %u MB ocupados por otras aplicaciones",
" VRAM: %u MB occupati da altre app",
" VRAM : %u Mo pris par d'autres applications",
" VRAM: %u MB von anderen Programmen belegt",
" VRAM: %u MB ocupados por outros apps",
" 显存:其他程序占用 %u MB"}},
{"Bloodborne - Graphics (Insert / hold L3+R3)###bbport_settings",
{"Bloodborne - Gráficos (Insert / mantener L3+R3)###bbport_settings",
"Bloodborne - Grafica (Insert / tieni premuto L3+R3)###bbport_settings",
+8 -2
View File
@@ -313,11 +313,13 @@ std::string FpsText() {
std::snprintf(text, sizeof(text), BbSettings::MenuText("%.0f FPS %.1f ms", "%.0f FPS %.1f мс"),
shown, frame_ms_avg);
}
// Other programs took video memory from the game (BufferCache::VramPromotionsPaused).
// Other programs took video memory from the game (BufferCache::VramPromotionsPaused): how much
// less the budget is than the most it was this session. It read "VRAM -552 MB", which looked
// like a negative figure.
if (const u32 lost = BbStats::vram_budget_lost_mb.load(std::memory_order_relaxed); lost >= 150) {
const size_t used = std::strlen(text);
std::snprintf(text + used, sizeof(text) - used,
BbSettings::MenuText(" VRAM -%u MB (other apps)", " VRAM −%u МБ (другие программы)"),
BbSettings::MenuText(" VRAM: %u MB taken by other apps", " VRAM: %u МБ заняты другими программами"),
lost);
}
return text;
@@ -1396,6 +1398,10 @@ bool MenuOpen() {
return menu_open || (AtlasOn() && initialized && BbAtlasMenu::Interacting());
}
bool TextPromptOpen() {
return prompt_active.load(std::memory_order_relaxed);
}
bool CapturesInput() {
// The text dialog too: keys typed into it (Backspace is the touchpad) stay out of the game.
// Hunter's Atlas: its open map and its controller shortcut while it is being held.
+2
View File
@@ -51,6 +51,8 @@ uint32_t RoutePad(uint32_t buttons, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t
/// The settings menu is open, or Hunter's Atlas's map or quick-menu tile uses the pointer: the
/// window lets the mouse go and shows the system cursor over it.
bool MenuOpen();
/// Any thread: the game's text dialog (ImeDialog, the character's name) is open.
bool TextPromptOpen();
/// Window thread: the game's text dialog (ImeDialog) state, drawn as a box over the frame.
void SetTextPrompt(bool active, const std::string& prompt, const std::string& text);
+1
View File
@@ -84,6 +84,7 @@ inline constexpr Effect Effects[] = {
{"tweak_no_camera_rotation", "No camera auto-rotation", "Без автоповорота камеры", false},
{"tweak_easy_run", "Run with less stick tilt", "Бег с меньшим наклоном стика", false},
{"tweak_ragdoll", "Dark Souls ragdoll physics", "Физика тел как в Dark Souls", false},
{"tweak_l3_run_jump", "L3 jumps while running", "L3: прыжок на бегу", false},
};
inline constexpr int EffectCount = int(sizeof(Effects) / sizeof(Effects[0]));
/// The game's own anti-aliasing effect (Effects index), derived from anti_aliasing.
+5
View File
@@ -60,6 +60,11 @@ private:
u64 last_mouse_motion_ms{}; ///< SDL_GetTicks of the last mouse motion (UpdateCursor)
bool cursor_hidden{};
u64 capture_settle_ms{}; ///< mouse motion before this (SDL_GetTicks) is not looking around
/// Issue #39: fullscreen asked again on Wayland when the compositor did not keep it (FixFullscreen).
bool fullscreen_wanted{};
u64 fullscreen_since_ms{}, fullscreen_tried_ms{};
int fullscreen_tries{};
void FixFullscreen();
bool pointer_shown{}; ///< a game module's pointer menu is open (bbgpu_pointer_wanted)
bool released_for_pointer{}; ///< the mouse was let go for it: taken back after
void CaptureMouse(bool capture);
+37 -4
View File
@@ -27,7 +27,11 @@ std::array<std::atomic<BbGpuEventFilter>, 4> event_filters{};
extern "C" int runtime_pad_steam_only(void) __attribute__((weak));
/// A keyboard or mouse event the game must not take (runtime_pad_steam_only): the gamepad's own
/// presses again. The port's menu (the overlay) keeps them, the trackpad as its pointer.
/// presses again. The port's menu (the overlay) keeps them, the trackpad as its pointer, and so
/// does the game's text dialog (the character's name): Steam's on-screen keyboard types into it
/// as text input and a keyboard plugged into the Deck as keys, and both were dropped there, so the
/// name could not be typed in Game Mode (upstream issue #443). The port's own on-screen keyboard
/// (bbport_overlay.cpp) still takes the gamepad.
bool SteamPadEcho(const SDL_Event& event) {
switch (event.type) {
case SDL_EVENT_KEY_DOWN:
@@ -37,7 +41,8 @@ bool SteamPadEcho(const SDL_Event& event) {
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL:
return runtime_pad_steam_only && runtime_pad_steam_only() && !BbOverlay::MenuOpen();
return runtime_pad_steam_only && runtime_pad_steam_only() && !BbOverlay::MenuOpen() &&
!BbOverlay::TextPromptOpen();
default:
return false;
}
@@ -223,8 +228,8 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
if (gamescope && !(fullscreen && fullscreen[0] == '1')) {
std::printf("Display: gamescope: fullscreen\n");
}
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN,
!placed && (gamescope || (fullscreen && fullscreen[0] == '1')));
fullscreen_wanted = !placed && (gamescope || (fullscreen && fullscreen[0] == '1'));
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_FULLSCREEN_BOOLEAN, fullscreen_wanted);
// Issue #234: the swapchain at the display's pixels on a scaled desktop (Wayland 200%), not at
// the logical size stretched by the compositor. Sizes below are read in pixels already.
SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
@@ -249,6 +254,7 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, const char* title) : width{width_}
window_info.render_surface = reinterpret_cast<void*>(SDL_GetNumberProperty(wp, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0));
} else if (driver && !std::strcmp(driver, "wayland")) {
window_info.type = WindowSystemType::Wayland;
fullscreen_since_ms = SDL_GetTicks();
window_info.display_connection = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, nullptr);
window_info.render_surface = SDL_GetPointerProperty(wp, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, nullptr);
} else {
@@ -519,9 +525,36 @@ bool WindowSDL::PollEvents() {
}
pointer_shown = pointer;
UpdateCursor();
FixFullscreen();
return is_open;
}
// Issue #39: on Wayland a program cannot place its window. With several monitors KWin can map
// it on another one first (the one SDL calls primary, not KDE's) and move it to the monitor
// asked for, and the window is left there as a plain window. Asked again, fullscreen holds (as
// KDE's own shortcut did). Only in the first seconds and a few times: there is no fullscreen
// toggle in the game, but the desktop's own shortcuts must keep working after that.
void WindowSDL::FixFullscreen() {
constexpr u64 FirstMs = 10000, BetweenMs = 1000;
constexpr int MaxTries = 3;
if (!fullscreen_wanted || !fullscreen_since_ms || fullscreen_tries >= MaxTries) {
return;
}
const u64 now = SDL_GetTicks();
if (now - fullscreen_since_ms > FirstMs) {
fullscreen_since_ms = 0; // past the start: the desktop decides from here
return;
}
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) || now - fullscreen_tried_ms < BetweenMs) {
return;
}
fullscreen_tried_ms = now;
++fullscreen_tries;
std::printf("Display: the compositor left the window out of fullscreen, asked again (%d of %d)\n",
fullscreen_tries, MaxTries);
SDL_SetWindowFullscreen(window, true);
}
// Issue #3: the OS cursor over the game. Hidden in fullscreen, and in a window after 3 s without
// moving the mouse; always shown while the settings menu or a game's pointer menu is open.
void WindowSDL::UpdateCursor() {
+15 -1
View File
@@ -251,9 +251,23 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_fmt) {
if (exact_fmt && info.pixel_format != resolved.guest_info.pixel_format) {
image_id = {}; // the very format is needed
} else if (resolved.guest_info.resources < info.resources) {
// bbport (Linux fork): which images, and whether the dropped one held what the GPU
// drew (then lost: the new image is read from guest memory). Issue #25: a texture
// garbled until the game was restarted.
const ImageInfo& had = resolved.guest_info;
LOG_WARNING(Render_Vulkan,
"Image overlap resolve failed: wanted {:#x}+{:#x} {}x{} {} levels {} layers {}; "
"dropped {:#x}+{:#x} {}x{} {} levels {} layers {}{}",
info.guest_address, info.guest_size, info.size.width, info.size.height,
vk::to_string(info.pixel_format), info.resources.levels, info.resources.layers,
had.guest_address, had.guest_size, had.size.width, had.size.height,
vk::to_string(had.pixel_format), had.resources.levels, had.resources.layers,
True(resolved.flags & ImageFlagBits::GpuModified)
? (resolved.SafeToDownload() ? ", drawn by the GPU (its contents are lost)"
: ", drawn by the GPU, then written")
: "");
FreeImage(image_id); // picked up wrongly
image_id = {};
LOG_WARNING(Render_Vulkan, "Image overlap resolve failed");
}
}
if (!image_id) {
+6 -1
View File
@@ -426,8 +426,13 @@ void TextureCache::GarbageCollectSamplers() {
return true;
}
--deletions;
sampler_lru.Free(samplers.at(key).lru_id);
auto& sampler = samplers.at(key);
const auto lru_id = sampler.lru_id;
// LRU age can select a sampler still referenced by render work (including age zero).
// Retain its owner through the current scheduler tick before destroying the VkSampler.
scheduler.DeferOperation([retired = std::move(sampler)] { (void)retired; });
samplers.erase(key);
sampler_lru.Free(lru_id);
return false;
};
configure(false);
+84 -21
View File
@@ -619,6 +619,8 @@ EN = {
"Бег примерно с 70% наклона стика вместо 90%": "Run from about 70% stick tilt instead of 90%",
"Физика тел как в Dark Souls": "Dark Souls ragdoll physics",
"Тела и предметы разлетаются от ударов": "Corpses and objects fly around when hit",
'L3: прыжок на бегу': 'L3 jumps while running',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': "While running with Circle held, L3 jumps as pressing Circle again does. Kyo's patch (from bbhost)",
# Add to Steam
"Добавить в Steam": "Add to Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "From the Steam library the game starts straight away, with this launcher's settings",
@@ -863,8 +865,11 @@ EN = {
'Вылеты при загрузке областей': 'Crashes while loading areas',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'The interface vanishing outside 1080p, motion vectors for the large beasts and a memory fault',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': 'Crashes when loading some maps, the gestures menu from the keyboard, and image changes for the upscaler',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': "The on-screen keyboard for the character's name and the shader compiling notice",
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Fast loading after a death or travel (bbhost) and The Hunter's Dream server",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': "The on-screen keyboard for the character's name, the shader compiling notice and compacting the memory copies in VRAM",
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Fast loading after a death or travel, L3 run-jump (bbhost) and The Hunter's Dream server",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Two GPU fixes: work submission counts, and samplers freed after their frame',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC: the guest no longer stays a spectator after a boss wipe',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'An unknown system value (sysctl) no longer stops the game',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'The online play libraries and the server for your own co-op',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'The groundwork for DLSS, and the FSR 4 model without trails behind characters',
'Апскейлер FSR 3.1': 'The FSR 3.1 upscaler',
@@ -890,7 +895,11 @@ EN = {
'Вылет «Transaction failed» на Seamless Hunt': 'The "Transaction failed" crash on Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'The "Transaction failed" crash on Seamless Hunt, the logs that stopped a BBSC guest from following a stranger\'s world, and testing BBSC 0.0.2 with a Windows player',
'Страница трофеев, не находившая файлы дампа': "The Trophies page not finding the dump's files",
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры и вылеты в менеджере памяти игры': "Logs and tests: a GPU freeze in a boss fight on NVIDIA, a crash in Yahar'gul, stretched models, a crash on the Steam Deck, effects turning black, rainbow textures and crashes in the game's memory manager",
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры, вылеты в менеджере памяти игры, падения частоты кадров на NVIDIA и выход на рабочий стол при игре по сети': "Logs and tests: a GPU freeze in a boss fight on NVIDIA, a crash in Yahar'gul, stretched models, a crash on the Steam Deck, effects turning black, rainbow textures, crashes in the game's memory manager, frame rate drops on NVIDIA and Exit to Desktop with online play",
'Запуск игры на встроенных видеокартах Intel Arc/Xe': 'The game starting on integrated Intel Arc/Xe GPUs',
'Журналы и проверки: процессоры AMD FX, полноэкранный режим в Wayland с несколькими мониторами и версия AppImage в Gear Lever': "Logs and tests: AMD FX processors, fullscreen on Wayland with several monitors and the AppImage's version in Gear Lever",
'Хранилище с клавиатурой и мышью': 'The storage box with keyboard and mouse',
'Журналы падения частоты кадров на NVIDIA': 'Logs of frame rate drops on NVIDIA',
'Отметка трофеев, уже полученных на PS4': 'Marking trophies already earned on the PS4',
'Предметы BBSC, которые не выдавались новому персонажу': "BBSC's items not given to a new character",
'Пустое окно лаунчера при запуске': "The launcher's blank window at start",
@@ -1529,6 +1538,8 @@ PT_BR = {
"Бег примерно с 70% наклона стика вместо 90%": "Corre com cerca de 70% de inclinação do analógico em vez de 90%",
"Физика тел как в Dark Souls": "Física de corpos como em Dark Souls",
"Тела и предметы разлетаются от ударов": "Corpos e objetos voam ao serem atingidos",
'L3: прыжок на бегу': 'L3 pula ao correr',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'Correndo com o círculo pressionado, L3 pula como apertar o círculo de novo. Patch de Kyo (do bbhost)',
# Add to Steam
"Добавить в Steam": "Adicionar ao Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Na biblioteca do Steam o jogo inicia direto, com as configurações deste launcher",
@@ -1949,8 +1960,11 @@ PT_BR = {
'Вылеты при загрузке областей': 'Travamentos ao carregar áreas',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'A interface que sumia fora de 1080p, vetores de movimento das grandes feras e uma falha de memória',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': 'Travamentos ao carregar alguns mapas, o menu de gestos pelo teclado e mudanças de imagem para o upscaler',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'O teclado na tela para o nome do personagem e o aviso de compilação de shaders',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Carregamento rápido após morrer ou viajar (bbhost) e o servidor The Hunter's Dream",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'O teclado na tela para o nome do personagem, o aviso de compilação de shaders e a compactação das cópias de memória na VRAM',
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Carregamento rápido após morrer ou viajar, pulo com L3 correndo (bbhost) e o servidor The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Duas correções da GPU: contagem do trabalho enviado e samplers liberados após o seu quadro',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC: o convidado não fica mais como espectador após uma derrota no chefe',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Um valor de sistema desconhecido (sysctl) não para mais o jogo',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'As bibliotecas do jogo online e o servidor para o seu próprio cooperativo',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'A base do suporte a DLSS e o modelo do FSR 4 sem rastros atrás dos personagens',
'Апскейлер FSR 3.1': 'O upscaler FSR 3.1',
@@ -1976,7 +1990,11 @@ PT_BR = {
'Вылет «Transaction failed» на Seamless Hunt': 'O travamento "Transaction failed" no Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'O crash "Transaction failed" no Seamless Hunt, os logs que fizeram o convidado do BBSC parar de seguir o mundo de um estranho e os testes do BBSC 0.0.2 com um jogador no Windows',
'Страница трофеев, не находившая файлы дампа': 'A página de troféus que não encontrava os arquivos do dump',
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры и вылеты в менеджере памяти игры': "Logs e testes: um congelamento da GPU numa luta contra chefe na NVIDIA, um crash em Yahar'gul, modelos esticados, um crash no Steam Deck, efeitos ficando pretos, texturas arco-íris e crashes no gerenciador de memória do jogo",
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры, вылеты в менеджере памяти игры, падения частоты кадров на NVIDIA и выход на рабочий стол при игре по сети': "Logs e testes: um congelamento da GPU numa luta contra chefe na NVIDIA, um crash em Yahar'gul, modelos esticados, um crash no Steam Deck, efeitos ficando pretos, texturas arco-íris, crashes no gerenciador de memória do jogo, quedas de FPS na NVIDIA e Sair para a área de trabalho jogando online",
'Запуск игры на встроенных видеокартах Intel Arc/Xe': 'O jogo iniciando em GPUs integradas Intel Arc/Xe',
'Журналы и проверки: процессоры AMD FX, полноэкранный режим в Wayland с несколькими мониторами и версия AppImage в Gear Lever': 'Logs e testes: processadores AMD FX, tela cheia no Wayland com vários monitores e a versão da AppImage no Gear Lever',
'Хранилище с клавиатурой и мышью': 'O baú de armazenamento com teclado e mouse',
'Журналы падения частоты кадров на NVIDIA': 'Logs de quedas de FPS na NVIDIA',
'Отметка трофеев, уже полученных на PS4': 'Marcar troféus já conquistados no PS4',
'Предметы BBSC, которые не выдавались новому персонажу': 'Itens do BBSC que não eram dados a um novo personagem',
'Пустое окно лаунчера при запуске': 'A janela em branco do launcher ao iniciar',
@@ -2620,6 +2638,8 @@ ES = {
"Бег примерно с 70% наклона стика вместо 90%": "Correr a partir de un 70 % de inclinación del stick en lugar del 90 %",
"Физика тел как в Dark Souls": "Física ragdoll de Dark Souls",
"Тела и предметы разлетаются от ударов": "Los cadáveres y los objetos salen volando al recibir golpes",
'L3: прыжок на бегу': 'L3 salta al correr',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'Corriendo con el círculo pulsado, L3 salta como volver a pulsar el círculo. Parche de Kyo (de bbhost)',
# Add to Steam
"Добавить в Steam": "Añadir a Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Desde la biblioteca de Steam el juego arranca directamente, con los ajustes de este launcher",
@@ -2950,8 +2970,11 @@ ES = {
'Вылеты при загрузке областей': 'Cierres al cargar zonas',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'La interfaz que desaparecía fuera de 1080p, los vectores de movimiento de las bestias grandes y un fallo de memoria',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': 'Cierres al cargar algunos mapas, el menú de gestos desde el teclado y cambios de imagen para el escalador',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'El teclado en pantalla para el nombre del personaje y el aviso de compilación de shaders',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Cargas rápidas tras morir o viajar (bbhost) y el servidor The Hunter's Dream",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'El teclado en pantalla para el nombre del personaje, el aviso de compilación de shaders y la compactación de las copias de memoria en la VRAM',
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Cargas rápidas tras morir o viajar, salto con L3 al correr (bbhost) y el servidor The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Dos arreglos de la GPU: el recuento del trabajo enviado y los samplers liberados tras su fotograma',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC: el invitado ya no se queda de espectador tras caer todos ante un jefe',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Un valor del sistema desconocido (sysctl) ya no detiene el juego',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Las librerías del juego online y el servidor para el cooperativo propio',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base del soporte de DLSS y el modelo de FSR 4 sin estelas tras los personajes',
'Апскейлер FSR 3.1': 'El escalado FSR 3.1',
@@ -2977,7 +3000,11 @@ ES = {
'Вылет «Transaction failed» на Seamless Hunt': 'El cierre «Transaction failed» en Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'El cierre «Transaction failed» en Seamless Hunt, los registros que hicieron que el invitado de BBSC dejara de seguir el mundo de un desconocido y las pruebas de BBSC 0.0.2 con un jugador en Windows',
'Страница трофеев, не находившая файлы дампа': 'La página de trofeos que no encontraba los archivos del volcado',
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры и вылеты в менеджере памяти игры': "Registros y pruebas: un cuelgue de la GPU en un jefe con NVIDIA, un cierre en Yahar'gul, modelos estirados, un cierre en Steam Deck, efectos que se vuelven negros, texturas arcoíris y cierres en el gestor de memoria del juego",
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры, вылеты в менеджере памяти игры, падения частоты кадров на NVIDIA и выход на рабочий стол при игре по сети': "Registros y pruebas: un cuelgue de la GPU en un jefe con NVIDIA, un cierre en Yahar'gul, modelos estirados, un cierre en Steam Deck, efectos que se vuelven negros, texturas arcoíris, cierres en el gestor de memoria del juego, bajadas de FPS con NVIDIA y Salir al escritorio jugando online",
'Запуск игры на встроенных видеокартах Intel Arc/Xe': 'El juego arrancando en gráficas integradas Intel Arc/Xe',
'Журналы и проверки: процессоры AMD FX, полноэкранный режим в Wayland с несколькими мониторами и версия AppImage в Gear Lever': 'Registros y pruebas: procesadores AMD FX, pantalla completa en Wayland con varios monitores y la versión de la AppImage en Gear Lever',
'Хранилище с клавиатурой и мышью': 'El almacén con teclado y ratón',
'Журналы падения частоты кадров на NVIDIA': 'Registros de bajadas de FPS con NVIDIA',
'Отметка трофеев, уже полученных на PS4': 'Marcar trofeos ya conseguidos en PS4',
'Предметы BBSC, которые не выдавались новому персонажу': 'Los objetos de BBSC que no recibía un personaje nuevo',
'Пустое окно лаунчера при запуске': 'La ventana en blanco del launcher al abrirlo',
@@ -3621,6 +3648,8 @@ IT = {
"Бег примерно с 70% наклона стика вместо 90%": "Corri inclinando lo stick di circa il 70% invece del 90%",
"Физика тел как в Dark Souls": "Fisica ragdoll di Dark Souls",
"Тела и предметы разлетаются от ударов": "Cadaveri e oggetti volano via quando vengono colpiti",
'L3: прыжок на бегу': 'L3 salta durante la corsa',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'Correndo con il cerchio premuto, L3 salta come premere di nuovo il cerchio. Patch di Kyo (da bbhost)',
# Add to Steam
"Добавить в Steam": "Aggiungi a Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Dalla libreria di Steam il gioco si avvia subito, con le impostazioni di questo launcher",
@@ -3951,8 +3980,11 @@ IT = {
'Вылеты при загрузке областей': 'Crash durante il caricamento delle aree',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'L’interfaccia che spariva fuori da 1080p, i vettori di movimento delle grandi bestie e un errore di memoria',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': "Crash al caricamento di alcune mappe, il menu dei gesti da tastiera e modifiche all'immagine per l'upscaler",
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'La tastiera su schermo per il nome del personaggio e l’avviso di compilazione degli shader',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Caricamenti rapidi dopo una morte o un viaggio (bbhost) e il server The Hunter's Dream",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'La tastiera su schermo per il nome del personaggio, l’avviso di compilazione degli shader e la compattazione delle copie di memoria nella VRAM',
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Caricamenti rapidi dopo una morte o un viaggio, salto con L3 in corsa (bbhost) e il server The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Due correzioni della GPU: il conteggio del lavoro inviato e i sampler liberati dopo il loro fotogramma',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': "BBSC: l'ospite non resta più spettatore dopo una sconfitta contro un boss",
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Un valore di sistema sconosciuto (sysctl) non ferma più il gioco',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Le librerie del gioco online e il server per la propria cooperativa',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base del supporto DLSS e il modello FSR 4 senza scie dietro i personaggi',
'Апскейлер FSR 3.1': 'L’upscaler FSR 3.1',
@@ -3978,7 +4010,11 @@ IT = {
'Вылет «Transaction failed» на Seamless Hunt': 'La chiusura "Transaction failed" su Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': "Il crash «Transaction failed» su Seamless Hunt, i log che hanno fatto smettere all'ospite di BBSC di seguire il mondo di uno sconosciuto e i test di BBSC 0.0.2 con un giocatore su Windows",
'Страница трофеев, не находившая файлы дампа': 'La pagina dei trofei che non trovava i file del dump',
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры и вылеты в менеджере памяти игры': "Log e test: un blocco della GPU in una battaglia contro un boss su NVIDIA, un crash a Yahar'gul, modelli stirati, un crash su Steam Deck, effetti che diventano neri, texture arcobaleno e crash nel gestore della memoria del gioco",
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры, вылеты в менеджере памяти игры, падения частоты кадров на NVIDIA и выход на рабочий стол при игре по сети': "Log e test: un blocco della GPU in una battaglia contro un boss su NVIDIA, un crash a Yahar'gul, modelli stirati, un crash su Steam Deck, effetti che diventano neri, texture arcobaleno, crash nel gestore della memoria del gioco, cali di FPS su NVIDIA ed Esci al desktop giocando online",
'Запуск игры на встроенных видеокартах Intel Arc/Xe': 'Il gioco che si avvia sulle GPU integrate Intel Arc/Xe',
'Журналы и проверки: процессоры AMD FX, полноэкранный режим в Wayland с несколькими мониторами и версия AppImage в Gear Lever': "Log e test: processori AMD FX, schermo intero su Wayland con più monitor e la versione dell'AppImage in Gear Lever",
'Хранилище с клавиатурой и мышью': 'Il deposito con tastiera e mouse',
'Журналы падения частоты кадров на NVIDIA': 'Log dei cali di FPS su NVIDIA',
'Отметка трофеев, уже полученных на PS4': 'Segnare i trofei già ottenuti su PS4',
'Предметы BBSC, которые не выдавались новому персонажу': 'Gli oggetti di BBSC non dati a un nuovo personaggio',
'Пустое окно лаунчера при запуске': "La finestra vuota del launcher all'avvio",
@@ -4622,6 +4658,8 @@ FR = {
"Бег примерно с 70% наклона стика вместо 90%": "Courir à partir d'environ 70 % d'inclinaison du stick au lieu de 90 %",
"Физика тел как в Dark Souls": "Physique ragdoll de Dark Souls",
"Тела и предметы разлетаются от ударов": "Les cadavres et les objets sont projetés quand on les frappe",
'L3: прыжок на бегу': 'L3 saute en courant',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'En courant avec rond maintenu, L3 saute comme un nouvel appui sur rond. Patch de Kyo (de bbhost)',
# Add to Steam
"Добавить в Steam": "Ajouter à Steam",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Depuis la bibliothèque Steam, le jeu démarre directement avec les réglages de ce launcher",
@@ -4952,8 +4990,11 @@ FR = {
'Вылеты при загрузке областей': 'Plantages au chargement des zones',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'L’interface qui disparaissait hors 1080p, les vecteurs de mouvement des grandes bêtes et une erreur mémoire',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': "Plantages au chargement de certaines cartes, le menu des gestes au clavier et des changements d'image pour l'upscaler",
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'Le clavier à l’écran pour le nom du personnage et l’avis de compilation des shaders',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Chargements rapides après une mort ou un voyage (bbhost) et le serveur The Hunter's Dream",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'Le clavier à l’écran pour le nom du personnage, l’avis de compilation des shaders et le compactage des copies de mémoire en VRAM',
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Chargements rapides après une mort ou un voyage, saut avec L3 en courant (bbhost) et le serveur The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Deux correctifs GPU : le décompte du travail soumis et les samplers libérés après leur image',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': "BBSC : l'invité ne reste plus spectateur après une défaite face à un boss",
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Une valeur système inconnue (sysctl) n’arrête plus le jeu',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Les bibliothèques du jeu en ligne et le serveur pour votre propre coopération',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'La base de la prise en charge de DLSS et le modèle FSR 4 sans traînées derrière les personnages',
'Апскейлер FSR 3.1': 'L’upscaler FSR 3.1',
@@ -4979,7 +5020,11 @@ FR = {
'Вылет «Transaction failed» на Seamless Hunt': 'Le plantage « Transaction failed » sur Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': "Le plantage « Transaction failed » sur Seamless Hunt, les journaux qui ont empêché l'invité de BBSC de suivre le monde d'un inconnu, et les tests de BBSC 0.0.2 avec un joueur sous Windows",
'Страница трофеев, не находившая файлы дампа': 'La page des trophées qui ne trouvait pas les fichiers du dump',
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры и вылеты в менеджере памяти игры': "Journaux et tests : un gel du GPU pendant un combat de boss sur NVIDIA, un plantage à Yahar'gul, des modèles étirés, un plantage sur Steam Deck, des effets devenus noirs, des textures arc-en-ciel et des plantages dans le gestionnaire de mémoire du jeu",
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры, вылеты в менеджере памяти игры, падения частоты кадров на NVIDIA и выход на рабочий стол при игре по сети': "Journaux et tests : un gel du GPU pendant un combat de boss sur NVIDIA, un plantage à Yahar'gul, des modèles étirés, un plantage sur Steam Deck, des effets devenus noirs, des textures arc-en-ciel, des plantages dans le gestionnaire de mémoire du jeu, des chutes de FPS sur NVIDIA et Quitter vers le bureau en jouant en ligne",
'Запуск игры на встроенных видеокартах Intel Arc/Xe': 'Le jeu qui démarre sur les GPU intégrés Intel Arc/Xe',
'Журналы и проверки: процессоры AMD FX, полноэкранный режим в Wayland с несколькими мониторами и версия AppImage в Gear Lever': "Journaux et tests : processeurs AMD FX, plein écran sous Wayland avec plusieurs écrans et la version de l'AppImage dans Gear Lever",
'Хранилище с клавиатурой и мышью': 'Le coffre de stockage au clavier et à la souris',
'Журналы падения частоты кадров на NVIDIA': 'Journaux de chutes de FPS sur NVIDIA',
'Отметка трофеев, уже полученных на PS4': 'Marquer les trophées déjà obtenus sur PS4',
'Предметы BBSC, которые не выдавались новому персонажу': 'Les objets de BBSC non donnés à un nouveau personnage',
'Пустое окно лаунчера при запуске': 'La fenêtre vide du launcher au démarrage',
@@ -5623,6 +5668,8 @@ DE = {
"Бег примерно с 70% наклона стика вместо 90%": "Rennen ab etwa 70 % Stick-Neigung statt 90 %",
"Физика тел как в Dark Souls": "Ragdoll-Physik wie in Dark Souls",
"Тела и предметы разлетаются от ударов": "Leichen und Objekte fliegen bei Treffern umher",
'L3: прыжок на бегу': 'L3 springt beim Rennen',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': 'Beim Rennen mit gehaltenem Kreis springt L3 wie ein erneuter Druck auf Kreis. Patch von Kyo (aus bbhost)',
# Add to Steam
"Добавить в Steam": "Zu Steam hinzufügen",
"Игра в библиотеке Steam запускается сразу, с настройками этого лаунчера": "Aus der Steam-Bibliothek startet das Spiel direkt, mit den Einstellungen dieses Launchers",
@@ -5953,8 +6000,11 @@ DE = {
'Вылеты при загрузке областей': 'Abstürze beim Laden von Gebieten',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': 'Die Oberfläche, die außerhalb von 1080p verschwand, Bewegungsvektoren der großen Bestien und ein Speicherfehler',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': 'Abstürze beim Laden einiger Karten, das Gestenmenü per Tastatur und Bildänderungen für den Upscaler',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': 'Die Bildschirmtastatur für den Charakternamen und der Hinweis zum Kompilieren der Shader',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "Schnelles Laden nach Tod oder Reise (bbhost) und der Server The Hunter's Dream",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': 'Die Bildschirmtastatur für den Charakternamen, der Hinweis zum Kompilieren der Shader und das Verdichten der Speicherkopien im VRAM',
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "Schnelles Laden nach Tod oder Reise, Sprung mit L3 beim Rennen (bbhost) und der Server The Hunter's Dream",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': 'Zwei GPU-Fehlerbehebungen: die Zählung der übermittelten Arbeit und Sampler, die nach ihrem Frame freigegeben werden',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC: der Gast bleibt nach einer Niederlage gegen einen Boss nicht mehr Zuschauer',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': 'Ein unbekannter Systemwert (sysctl) hält das Spiel nicht mehr an',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': 'Die Bibliotheken für das Online-Spiel und der Server für den eigenen Koop',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'Die Grundlage für DLSS und das FSR-4-Modell ohne Schlieren hinter Figuren',
'Апскейлер FSR 3.1': 'Der FSR-3.1-Upscaler',
@@ -5980,7 +6030,11 @@ DE = {
'Вылет «Transaction failed» на Seamless Hunt': 'Der Absturz „Transaction failed“ auf Seamless Hunt',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'Der Absturz „Transaction failed“ auf Seamless Hunt, die Logs, durch die ein BBSC-Gast nicht mehr der Welt eines Fremden folgt, und Tests von BBSC 0.0.2 mit einem Windows-Spieler',
'Страница трофеев, не находившая файлы дампа': 'Die Trophäenseite, die die Dateien des Dumps nicht fand',
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры и вылеты в менеджере памяти игры': "Logs und Tests: ein GPU-Hänger in einem Bosskampf auf NVIDIA, ein Absturz in Yahar'gul, verzerrte Modelle, ein Absturz auf dem Steam Deck, schwarz werdende Effekte, Regenbogentexturen und Abstürze in der Speicherverwaltung des Spiels",
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры, вылеты в менеджере памяти игры, падения частоты кадров на NVIDIA и выход на рабочий стол при игре по сети': "Logs und Tests: ein GPU-Hänger in einem Bosskampf auf NVIDIA, ein Absturz in Yahar'gul, verzerrte Modelle, ein Absturz auf dem Steam Deck, schwarz werdende Effekte, Regenbogentexturen, Abstürze in der Speicherverwaltung des Spiels, FPS-Einbrüche auf NVIDIA und Zum Desktop beenden im Online-Spiel",
'Запуск игры на встроенных видеокартах Intel Arc/Xe': 'Der Spielstart auf integrierten Intel-Arc/Xe-GPUs',
'Журналы и проверки: процессоры AMD FX, полноэкранный режим в Wayland с несколькими мониторами и версия AppImage в Gear Lever': 'Logs und Tests: AMD-FX-Prozessoren, Vollbild unter Wayland mit mehreren Monitoren und die Version des AppImage in Gear Lever',
'Хранилище с клавиатурой и мышью': 'Die Lagerkiste mit Tastatur und Maus',
'Журналы падения частоты кадров на NVIDIA': 'Logs von FPS-Einbrüchen auf NVIDIA',
'Отметка трофеев, уже полученных на PS4': 'Auf der PS4 bereits erhaltene Trophäen markieren',
'Предметы BBSC, которые не выдавались новому персонажу': 'BBSC-Gegenstände, die ein neuer Charakter nicht bekam',
'Пустое окно лаунчера при запуске': 'Das leere Launcher-Fenster beim Start',
@@ -6215,6 +6269,8 @@ ZH_CN = {
'Бег примерно с 70% наклона стика вместо 90%': '摇杆推到约 70% 即可奔跑,而不是 90%',
'Физика тел как в Dark Souls': '黑暗之魂式布娃娃物理',
'Тела и предметы разлетаются от ударов': '尸体和物品被击中时会飞散',
'L3: прыжок на бегу': '奔跑时按 L3 跳跃',
'На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)': '按住圆圈奔跑时,L3 会像再次按下圆圈一样跳跃。Kyo 的补丁(来自 bbhost)',
'shadnet-p2p: на вашем ПК или VPS': 'shadnet-p2p:在你的电脑或 VPS 上',
'Платиновый': '白金',
'Золотой': '金',
@@ -6923,8 +6979,11 @@ ZH_CN = {
'Вылеты при загрузке областей': '加载区域时的崩溃',
'Интерфейс, пропадавший не в 1080p, векторы движения больших чудовищ и ошибка памяти': '非 1080p 下消失的界面、大型野兽的运动矢量与一处内存错误',
'Вылеты при загрузке некоторых карт, меню жестов с клавиатуры и изменения изображения для апскейлера': '加载部分地图时的崩溃、用键盘打开的手势菜单,以及超分辨率的画面改动',
'Экранная клавиатура для имени персонажа и уведомление о компиляции шейдеров': '角色名的屏幕键盘与着色器编译提示',
"Быстрая загрузка после смерти и перемещения (bbhost) и сервер The Hunter's Dream": "死亡或传送后的快速加载(bbhost)与 The Hunter's Dream 服务器",
'Экранная клавиатура для имени персонажа, уведомление о компиляции шейдеров и уплотнение копий памяти в видеопамяти': '角色名的屏幕键盘、着色器编译提示,以及显存中内存副本的压缩',
"Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу (bbhost) и сервер The Hunter's Dream": "死亡或传送后的快速加载、奔跑时 L3 跳跃(bbhost)与 The Hunter's Dream 服务器",
'Два исправления GPU: счётчики отправленной работы и сэмплеры, освобождаемые после их кадра': '两项 GPU 修复:已提交工作的计数,以及在其帧结束后才释放的采样器',
'BBSC: гость больше не остаётся зрителем после гибели группы у босса': 'BBSC:首领战全灭后,客人不再一直处于观战状态',
'Неизвестный системный параметр (sysctl) больше не останавливает игру': '未知的系统值(sysctl)不再导致游戏停止',
'Сетевые библиотеки игры по сети и сервер для своего кооператива': '联机库与自建合作服务器',
'Основа поддержки DLSS и модель FSR 4 без шлейфов за персонажами': 'DLSS 支持的基础,以及角色身后无拖影的 FSR 4 模型',
'Апскейлер FSR 3.1': 'FSR 3.1 超分辨率',
@@ -6950,7 +7009,11 @@ ZH_CN = {
'Вылет «Transaction failed» на Seamless Hunt': 'Seamless Hunt 上的“Transaction failed”崩溃',
'Вылет «Transaction failed» на Seamless Hunt, журналы, по которым гость BBSC перестал следовать за чужим хозяином, и проверки BBSC 0.0.2 с игроком на Windows': 'Seamless Hunt 上的“Transaction failed”崩溃、让 BBSC 访客不再跟随陌生人世界的日志,以及与 Windows 玩家一起测试 BBSC 0.0.2',
'Страница трофеев, не находившая файлы дампа': '找不到转储文件的奖杯页面',
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры и вылеты в менеджере памяти игры': "日志与测试:NVIDIA 上首领战中的 GPU 卡死、Yahar'gul 的崩溃、被拉伸的模型、Steam Deck 上的崩溃、变黑的特效、彩虹色纹理,以及游戏内存管理器中的崩溃",
'Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, радужные текстуры, вылеты в менеджере памяти игры, падения частоты кадров на NVIDIA и выход на рабочий стол при игре по сети': "日志与测试:NVIDIA 上首领战中的 GPU 卡死、Yahar'gul 的崩溃、被拉伸的模型、Steam Deck 上的崩溃、变黑的特效、彩虹色纹理、游戏内存管理器中的崩溃、NVIDIA 上的帧率下降,以及联机时的退出到桌面",
'Запуск игры на встроенных видеокартах Intel Arc/Xe': '游戏在 Intel Arc/Xe 集成显卡上启动',
'Журналы и проверки: процессоры AMD FX, полноэкранный режим в Wayland с несколькими мониторами и версия AppImage в Gear Lever': '日志与测试:AMD FX 处理器、Wayland 多显示器下的全屏,以及 Gear Lever 中显示的 AppImage 版本',
'Хранилище с клавиатурой и мышью': '使用键盘和鼠标操作储物箱',
'Журналы падения частоты кадров на NVIDIA': 'NVIDIA 上帧率下降的日志',
'Отметка трофеев, уже полученных на PS4': '标记已在 PS4 上获得的奖杯',
'Предметы BBSC, которые не выдавались новому персонажу': '新角色拿不到 BBSC 物品的问题',
'Пустое окно лаунчера при запуске': '启动时启动器窗口空白的问题',
+17 -5
View File
@@ -160,6 +160,8 @@ TWEAKS = [
"Камера не поворачивается сама при движении; ручное управление и захват цели работают"),
("tweak_easy_run", "Бег с меньшим наклоном стика", "Бег примерно с 70% наклона стика вместо 90%"),
("tweak_ragdoll", "Физика тел как в Dark Souls", "Тела и предметы разлетаются от ударов"),
("tweak_l3_run_jump", "L3: прыжок на бегу",
"На бегу с зажатым кругом L3 прыгает, как повторное нажатие круга. Патч Kyo (из bbhost)"),
]
# Loading and smoothness (patches from bbhost, applied at start): bbport.ini key, title, subtitle,
# default.
@@ -1031,10 +1033,15 @@ THANKS = (
_github("GoncaloLobo0", "Вылеты при загрузке областей"),
_github("bmy", "Векторы движения больших чудовищ и ошибка памяти"),
_github("Mrsuss60", "Меню жестов с клавиатуры, слот удалённого изображения и журнал неудачных резервирований памяти"),
_github("SrShadowy", "Экранная клавиатура для имени персонажа и уведомление о компиляции "
"шейдеров"),
_github("droogie", "Быстрая загрузка после смерти и перемещения (bbhost) и сервер "
"The Hunter's Dream"),
_github("SrShadowy", "Экранная клавиатура для имени персонажа, уведомление о компиляции "
"шейдеров и уплотнение копий памяти в видеопамяти"),
_github("Wooloomooloo2", "Запуск игры на встроенных видеокартах Intel Arc/Xe"),
_github("droogie", "Быстрая загрузка после смерти и перемещения, прыжок L3 на бегу "
"(bbhost) и сервер The Hunter's Dream"),
_github("Asanilo", "Два исправления GPU: счётчики отправленной работы и сэмплеры, "
"освобождаемые после их кадра"),
_github("AlexanderDzhoganov", "BBSC: гость больше не остаётся зрителем после гибели группы у босса"),
_github("nullsumlabs", "Неизвестный системный параметр (sysctl) больше не останавливает игру"),
_github("Wozzardman", "Сервер для своего кооператива и бесшовный призыв BBSC"),
_github("IFreemz", "Модель FSR 4 без шлейфов за персонажами"),
_github("FireBurn", "Апскейлер FSR 3.1"),
@@ -1079,7 +1086,8 @@ THANKS = (
_github("simo1730", "Страница трофеев, не находившая файлы дампа"),
_github("Nahnahchi", "Журналы и проверки: зависание GPU в битве с боссом на NVIDIA, вылет "
"в Яхар’гуле, растянутые модели, вылет на Steam Deck, чёрные эффекты, "
"радужные текстуры и вылеты в менеджере памяти игры"),
"радужные текстуры, вылеты в менеджере памяти игры, падения частоты "
"кадров на NVIDIA и выход на рабочий стол при игре по сети"),
_github("thomasgarafano1", "Hunter's Atlas, не готовивший карту с дампом GOTY, и проверка исправления"),
_github("TacticalBill", "Отметка трофеев, уже полученных на PS4"),
_github("holydingus", "Предметы BBSC, которые не выдавались новому персонажу"),
@@ -1088,6 +1096,10 @@ THANKS = (
_github("BlazinKarp", "Оверлей Steam в игре"),
_github("molnar-a", "Вылет на Steam Deck после путешествия через фонарь и проверка исправления"),
_github("Aman9das", "Просьба о раскладке клавиатуры из bbport 0.6"),
_github("eberhardweber", "Журналы и проверки: процессоры AMD FX, полноэкранный режим в "
"Wayland с несколькими мониторами и версия AppImage в Gear Lever"),
_github("GitBire", "Хранилище с клавиатурой и мышью"),
_github("npc-xc", "Журналы падения частоты кадров на NVIDIA"),
)),
("Другие благодарности", (
("Dommo, goomab", "Иконки кнопок Xbox и Switch (Nexus Mods)",
+1 -1
View File
@@ -14,7 +14,7 @@ import re
import tempfile
import urllib.request
VERSION = "0.70-linux.1"
VERSION = "0.70-linux.5"
def _build():
+37
View File
@@ -89,6 +89,43 @@ EOF
chmod +x "$appdir/bin/bloodborne"
sed -e 's|@LAUNCHER@|bloodborne|' -e 's|@ICON@|bloodborne|' \
launcher/io.github.bbport.Launcher.desktop.in > "$work/bloodborne.desktop"
# The version and a description for AppImage managers (Gear Lever showed "UNKNOWN", #49): the
# desktop entry's X-AppImage-Version, which appimagetool writes from VERSION (it replaces one in
# the entry), and AppStream metainfo named after the desktop entry. The version is the
# launcher's own (bbport_update.py), so it is kept in one place.
version=$(sed -n 's/^VERSION = "\(.*\)"$/\1/p' launcher/bbport_update.py)
[[ -n $version ]] || { echo "No VERSION in launcher/bbport_update.py" >&2; exit 1; }
export VERSION=$version
mkdir -p "$appdir/share/metainfo"
cat > "$appdir/share/metainfo/bloodborne.appdata.xml" <<METAINFO
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>io.github.ayozetr.bloodborne_linux</id>
<name>Bloodborne (bbport)</name>
<developer id="io.github.ayozetr">
<name>bloodborne_linux contributors</name>
</developer>
<summary>Native Linux port launcher for your own copy of Bloodborne</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<description>
<p>Runs Bloodborne natively on Linux from your own decrypted dump of the PS4 game with update
1.09. No game files are included. Upscaling (DLSS, FSR), online play, mods and the community
patches are set up from the launcher, which also updates itself.</p>
</description>
<launchable type="desktop-id">bloodborne.desktop</launchable>
<url type="homepage">https://github.com/ayozetr/bloodborne_linux</url>
<url type="bugtracker">https://github.com/ayozetr/bloodborne_linux/issues</url>
<categories><category>Game</category></categories>
<content_rating type="oars-1.1">
<content_attribute id="violence-fantasy">intense</content_attribute>
<content_attribute id="violence-bloodshed">intense</content_attribute>
</content_rating>
<releases>
<release version="$version" date="$(date +%F)"/>
</releases>
</component>
METAINFO
# GTK, libadwaita and their typelibs are loaded by PyGObject at run time (not linked): named here.
# So are Python's ssl and hashlib modules, which quick-sharun leaves out by default: the launcher's
+12
View File
@@ -3492,4 +3492,16 @@
<Line Type="bytes" Address="0x018281A8" Value="C783500E000002000000"/>
</PatchList>
</Metadata>
<Metadata Title="Bloodborne" Name="L3 Run-Jump" Note="While running with Circle held, L3 jumps as letting go of Circle and pressing it again does; Circle is unchanged. Works with any c0000.hks that keeps the game's run-jump (vanilla, Hyperborne); not with the Nightreign Movement scripts. Uses one byte at 0x05AD3D00 in the zeroed tail of .bss as a 3-frame window." Author="Kyo (from droogie/bbhost)" PatchVer="1.0" AppVer="01.09" AppElf="eboot.bin" isEnabled="false">
<PatchList>
<!-- PadManipulator::Update: the call of ActionRequest::Update goes through the new code; was E8 BA 71 4E 00 -->
<Line Type="bytes" Address="0x01928751" Value="E8AA68B903"/>
<!-- the script's SpEffect query (env 56) goes through the new code; was 48 8B 8B C8 01 00 00 -->
<Line Type="bytes" Address="0x01E142F1" Value="E98AAD6A039090"/>
<!-- new code in the zero tail of the code segment: L3 pressed while running adds the Rolling request; was zeros -->
<Line Type="bytes" Address="0x054BF000" Value="803DF94C6100007406FE0DF14C6100F64711207444F6471920753EF64710207438488B47084885C0742F488B80C80100004885C07423488B40084885C0741A817840870100007406488B4058EBEC804F1202C605A74C610003E9B20895FC"/>
<!-- new code: SpEffect 390 counts as on for 3 frames after it; was zeros -->
<Line Type="bytes" Address="0x054BF080" Value="3D86010000752C803D724C6100007423488B0DE1F747004885C97417483B59607511488B3D374B3200BE01000000E9976695FC488B8BC8010000E9395295FC"/>
</PatchList>
</Metadata>
</Patch>
+1
View File
@@ -62,6 +62,7 @@ EFFECTS={
'tweak_no_camera_rotation':(None,'Disable Camera Auto Rotation via Movement'),
'tweak_easy_run':(None,'Sensitive Analog Input (easier to run)'),
'tweak_ragdoll':(None,'DS1-like physics'),
'tweak_l3_run_jump':(None,'L3 Run-Jump'),
# Loading and smoothness (from droogie/bbhost); the first two are on unless turned off.
'tweak_quick_reentry':(None,'Quick Re-entry'),
'tweak_fast_deflate':(None,'Fast Run-time Deflate'),
+29 -3
View File
@@ -14,6 +14,7 @@
#include "runtime_trophy.h"
#include "runtime_map.h"
#include "runtime_atlas_state.h"
#include "runtime_movbe.h"
#include "gpu/bbgpu.h"
#if !defined(__x86_64__) || !defined(__GNUC__)
#error This prototype requires x86-64 GCC or Clang (including MinGW).
@@ -100,8 +101,12 @@ static void print_environment(void) {
/* The game's code was compiled for the PS4's CPU (AMD Jaguar) and runs as it is: AVX, BMI1
* (andn/bextr/blsr/tzcnt), MOVBE, LZCNT and POPCNT, thousands of each in eboot.bin. A CPU without
* them stops at the first one with SIGILL (exit code 132, issue #26), and lzcnt/tzcnt even run as
* bsr/bsf there, with other results. Said before the game starts; BB_SKIP_CPU_CHECK=1 skips it. */
* bsr/bsf there, with other results. Said before the game starts; BB_SKIP_CPU_CHECK=1 skips it.
* MOVBE alone is no obstacle: the port runs those itself (runtime_movbe.c), so AMD's FX
* processors, which lack only that one, play (issue #39). */
static void check_cpu(void) {
if (!runtime_movbe_supported())
printf("CPU: no MOVBE; the port runs the game's movbe itself (runtime_movbe.c)\n");
const char *skip = bb_env("BB_SKIP_CPU_CHECK");
if (skip && !strcmp(skip, "1")) return;
unsigned a, b, c, d, leaf1_c = 0, leaf7_b = 0, ext1_c = 0;
@@ -115,7 +120,7 @@ static void check_cpu(void) {
avx = (lo & 6) == 6; /* the OS saves SSE and AVX state */
}
const struct { int present; const char *name; } features[] = {
{avx, "AVX"}, {(leaf7_b & bit_BMI) != 0, "BMI1"}, {(leaf1_c & bit_MOVBE) != 0, "MOVBE"},
{avx, "AVX"}, {(leaf7_b & bit_BMI) != 0, "BMI1"},
{(ext1_c & bit_LZCNT) != 0, "LZCNT"}, {(leaf1_c & bit_POPCNT) != 0, "POPCNT"},
{(leaf1_c & bit_SSE4_2) != 0, "SSE4.2"},
};
@@ -129,7 +134,8 @@ static void check_cpu(void) {
fprintf(stderr,
"ERROR: this CPU lacks %s. Bloodborne's code was compiled for the PS4's CPU and uses "
"these instructions directly: it needs an Intel Haswell (4th generation Core, 2013) or "
"newer, or an AMD Ryzen. BB_SKIP_CPU_CHECK=1 starts anyway.\n", missing);
"newer, or an AMD FX-4300, 6300 or 8300 series (Piledriver) or newer. "
"BB_SKIP_CPU_CHECK=1 starts anyway.\n", missing);
exit(1);
}
static uint64_t read64(FILE *f) {
@@ -244,6 +250,23 @@ void bb_fault_wait(const char *what) {
#endif
}
static void fault(int sig, siginfo_t *info, void *context) {
#ifndef _WIN32
/* A movbe on a processor without MOVBE (AMD FX, issue #39): run by the port, the game goes on. */
if (sig == SIGILL) {
greg_t *g = ((ucontext_t *)context)->uc_mcontext.gregs;
static const int order[16] = {REG_RAX, REG_RCX, REG_RDX, REG_RBX, REG_RSP, REG_RBP, REG_RSI, REG_RDI,
REG_R8, REG_R9, REG_R10, REG_R11, REG_R12, REG_R13, REG_R14, REG_R15};
uint64_t regs[16], rip = (uint64_t)g[REG_RIP];
if (rip - (uintptr_t)image < 0x10000000) {
for (int i = 0; i < 16; ++i) regs[i] = (uint64_t)g[order[i]];
if (runtime_movbe_fault(regs, &rip)) {
for (int i = 0; i < 16; ++i) g[order[i]] = (greg_t)regs[i];
g[REG_RIP] = (greg_t)rip;
return;
}
}
}
#endif
/* GPU page tracking (write-protected guest pages) is resolved first. */
if (gpu_enabled && sig == SIGSEGV && bbgpu_handle_fault(context, info->si_addr)) return;
/* A speculative guest memory read (runtime_memory.c) failed: resume its recovery point. */
@@ -810,6 +833,9 @@ int main(int argc, char **argv) {
runtime_atlas_state_bind(image, size);
}
}
#ifndef _WIN32
runtime_movbe_bind(image, size); /* BB_MOVBE_TRAP (testing): movbe made ud2 */
#endif
#ifdef _WIN32
patch_guest_tls(segments, ns);
#endif
+7
View File
@@ -99,6 +99,13 @@ void runtime_file_report(void);
void runtime_file_configure(const char *app0, const char *user);
int runtime_file_mount(const char *guest, const char *host);
void runtime_file_unmount(const char *guest);
/* Exit to desktop from the world (runtime_services.c): armed by the game menu's entry, the game
* closes once it is back at the title with its save written. */
void runtime_quit_arm(int on);
void runtime_quit_at_title(void);
void runtime_quit_on_open(const char *guest);
void runtime_quit_on_save_done(void);
int runtime_savedata_mounted(void);
int runtime_file_translate(const char *guest, char *out, size_t size);
int64_t runtime_file_open(const char *path, int flags, int mode);
int64_t runtime_file_close(int fd);
-3
View File
@@ -1,3 +0,0 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#define BB_ATLAS_BUILD_JSON "{\"adapter_protocol\":1,\"upstream_release\":\"windows-v1.8.1\",\"upstream_commit\":\"bee7ec16213022f15bf34084b5587f1ff09e0e65\",\"feature_schema\":1}"
+1
View File
@@ -508,6 +508,7 @@ static void presence_map(const char *guest) {
pthread_mutex_unlock(&presence_lock);
}
static int64_t do_open(const char *guest,int flags,int mode) {
runtime_quit_on_open(guest);
if (game_path(guest) && (flags & (3|0x8|0x200|0x400|0x800))) return -EROFS;
char path[1024];
int e=translate(guest,path,sizeof(path));
+11 -4
View File
@@ -254,10 +254,17 @@ static ABI int32_t guest_sysctl(const int32_t *name,uint32_t namelen,void *old,u
if (old) { if (*oldlen<4) return fail_posix(ENOMEM); memcpy(old,&value,4); }
*oldlen=4; return 0;
}
fprintf(stderr,"STOP: unsupported sysctl mib");
for (uint32_t i=0;i<namelen && i<8;++i) fprintf(stderr," %d",name[i]);
fputc('\n',stderr);
exit(21);
/* Any other name: ENOENT, as FreeBSD answers a name it does not know, instead of stopping the
* game (exit 21). The first few are logged with their mib. From the nullsumlabs fork
* (bloodborne_pc-deck-optimized), after Awakenchan's Windows port. */
static int logged;
if (logged<8) {
++logged;
fprintf(stderr,"Runtime: sysctl mib");
for (uint32_t i=0;i<namelen && i<8;++i) fprintf(stderr," %d",name[i]);
fprintf(stderr," is not known: ENOENT\n");
}
return fail_posix(ENOENT);
}
/* ---- pthread_once and thread-specific data ---- */
+210
View File
@@ -0,0 +1,210 @@
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
/* MOVBE on processors without it. The game's code was built for the PS4's CPU (AMD Jaguar) and
* has 987 movbe (a load or store with its bytes swapped: the big-endian headers of the game's
* files, network data). AMD's FX processors (Bulldozer to Piledriver) have every other instruction
* the game uses, but not this one: the first movbe stopped the game with SIGILL (issue #39). The
* SIGILL handler (probe.c) does it here instead and resumes after it; on a processor with MOVBE
* this code never runs.
*
* BB_MOVBE_TRAP=file (testing on any processor): the image offsets of movbe in the file
* (one hex number per line, tools/movbe_sites.py) become ud2, which this code runs as the movbe
* that was there, as a processor without MOVBE would. */
#define _GNU_SOURCE
#include "runtime_movbe.h"
#include "../gpu/shim/bb_env.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
/* x86 register numbers (rax 0 ... r15 15). */
enum { RSP = 4, RBP = 5 };
int runtime_movbe_run(const uint8_t *code, uint64_t regs[16], uint64_t rip) {
const uint8_t *p = code;
int rex = 0, operand16 = 0;
for (;; ++p) {
if (p - code >= 4) return 0;
if (*p == 0x66) operand16 = 1;
else break;
}
if ((*p & 0xf0) == 0x40) rex = *p++;
if (p[0] != 0x0f || p[1] != 0x38 || (p[2] != 0xf0 && p[2] != 0xf1)) return 0;
const int store = p[2] == 0xf1;
p += 3;
const uint8_t modrm = *p++;
const int mod = modrm >> 6, rm = modrm & 7;
const int reg = ((modrm >> 3) & 7) | (rex & 4 ? 8 : 0);
if (mod == 3) return 0; /* the register form does not exist */
uint64_t address = 0;
int rip_relative = 0;
if (rm == 4) {
const uint8_t sib = *p++;
const int scale = sib >> 6, index = ((sib >> 3) & 7) | (rex & 2 ? 8 : 0);
const int base = (sib & 7) | (rex & 1 ? 8 : 0);
if (index != RSP) address += regs[index] << scale;
if ((base & 7) == RBP && mod == 0) {
int32_t disp;
memcpy(&disp, p, 4);
p += 4;
address += (uint64_t)(int64_t)disp;
} else {
address += regs[base];
}
} else if (rm == 5 && mod == 0) {
rip_relative = 1;
} else {
address = regs[rm | (rex & 1 ? 8 : 0)];
}
if (mod == 1) {
address += (uint64_t)(int64_t)(int8_t)*p++;
} else if (mod == 2 || rip_relative) {
int32_t disp;
memcpy(&disp, p, 4);
p += 4;
address += (uint64_t)(int64_t)disp;
}
const int length = (int)(p - code);
if (rip_relative) address += rip + (uint64_t)length;
uint8_t *memory = (uint8_t *)(uintptr_t)address;
if (rex & 8) {
uint64_t value;
if (store) {
value = __builtin_bswap64(regs[reg]);
memcpy(memory, &value, 8);
} else {
memcpy(&value, memory, 8);
regs[reg] = __builtin_bswap64(value);
}
} else if (operand16) {
uint16_t value;
if (store) {
value = __builtin_bswap16((uint16_t)regs[reg]);
memcpy(memory, &value, 2);
} else {
memcpy(&value, memory, 2);
regs[reg] = (regs[reg] & ~(uint64_t)0xffff) | __builtin_bswap16(value);
}
} else {
uint32_t value;
if (store) {
value = __builtin_bswap32((uint32_t)regs[reg]);
memcpy(memory, &value, 4);
} else {
memcpy(&value, memory, 4);
regs[reg] = __builtin_bswap32(value); /* a 32-bit result clears the upper half */
}
}
return length;
}
/* BB_MOVBE_TRAP: the replaced instructions, by image offset. */
typedef struct {
uint64_t offset;
uint8_t code[16];
} Trapped;
static Trapped *trapped;
static size_t trapped_count;
static uintptr_t trapped_image;
static int by_offset(const void *a, const void *b) {
const uint64_t x = ((const Trapped *)a)->offset, y = ((const Trapped *)b)->offset;
return x < y ? -1 : x > y;
}
void runtime_movbe_bind(unsigned char *image, uint64_t size) {
const char *path = bb_env("BB_MOVBE_TRAP");
if (!path || !path[0]) return;
FILE *f = fopen(path, "r");
if (!f) {
printf("MOVBE: BB_MOVBE_TRAP=%s cannot be read\n", path);
return;
}
size_t capacity = 0, skipped = 0;
char line[64];
const uint64_t page = (uint64_t)sysconf(_SC_PAGESIZE);
while (fgets(line, sizeof(line), f)) {
char *end;
const uint64_t offset = strtoull(line, &end, 16);
if (end == line) continue;
if (offset + 16 > size) {
++skipped;
continue;
}
Trapped t = {.offset = offset};
memcpy(t.code, image + offset, sizeof(t.code));
const uint8_t *op = t.code + ((t.code[0] & 0xf0) == 0x40); /* past a REX prefix */
if (op[0] != 0x0f || op[1] != 0x38 || (op[2] != 0xf0 && op[2] != 0xf1)) {
++skipped;
continue;
}
if (trapped_count == capacity) {
capacity = capacity ? capacity * 2 : 1024;
trapped = realloc(trapped, capacity * sizeof(*trapped));
if (!trapped) {
fclose(f);
trapped_count = 0;
return;
}
}
trapped[trapped_count++] = t;
const uint64_t first = offset & ~(page - 1);
mprotect(image + first, (size_t)((offset + 2 + page - 1) / page * page - first), PROT_READ | PROT_WRITE | PROT_EXEC);
image[offset] = 0x0f;
image[offset + 1] = 0x0b; /* ud2 */
}
fclose(f);
qsort(trapped, trapped_count, sizeof(*trapped), by_offset);
trapped_image = (uintptr_t)image;
printf("MOVBE: BB_MOVBE_TRAP: %zu movbe made ud2 and run by the port (%zu lines skipped)\n", trapped_count,
skipped);
}
int runtime_movbe_supported(void) {
unsigned a, b, c, d;
__asm__("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(1), "c"(0));
return (c >> 22) & 1;
}
/* How many the port ran (the log says it every 10 s). */
static volatile uint64_t emulated;
int runtime_movbe_fault(uint64_t regs[16], uint64_t *rip) {
const uint8_t *code = (const uint8_t *)(uintptr_t)*rip;
if (trapped_count && code[0] == 0x0f && code[1] == 0x0b) {
const Trapped key = {.offset = *rip - trapped_image};
const Trapped *t = bsearch(&key, trapped, trapped_count, sizeof(*trapped), by_offset);
if (t) code = t->code;
}
const int length = runtime_movbe_run(code, regs, *rip);
if (!length) return 0;
*rip += (uint64_t)length;
const uint64_t total = __atomic_add_fetch(&emulated, 1, __ATOMIC_RELAXED);
/* In the log: the first one, then how many every 10 s (a signal each: a cost if many). */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
static int64_t next_report;
static uint64_t reported;
int64_t due = __atomic_load_n(&next_report, __ATOMIC_RELAXED);
if (total == 1 || (now.tv_sec >= due &&
__atomic_compare_exchange_n(&next_report, &due, (int64_t)now.tv_sec + 10, 0,
__ATOMIC_RELAXED, __ATOMIC_RELAXED))) {
if (total == 1) __atomic_store_n(&next_report, (int64_t)now.tv_sec + 10, __ATOMIC_RELAXED);
char line[96] = "MOVBE: run by the port: ";
size_t at = strlen(line);
char digits[24];
size_t n = 0;
uint64_t value = total - __atomic_exchange_n(&reported, total, __ATOMIC_RELAXED);
do digits[n++] = (char)('0' + value % 10); while ((value /= 10) && n < sizeof(digits));
while (n) line[at++] = digits[--n];
static const char tail[] = " (this processor has no MOVBE)\n";
memcpy(line + at, tail, sizeof(tail) - 1);
ssize_t written = write(1, line, at + sizeof(tail) - 1);
(void)written;
}
return 1;
}
+17
View File
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
/* MOVBE on processors without it (runtime_movbe.c). */
#ifndef BBPORT_RUNTIME_MOVBE_H
#define BBPORT_RUNTIME_MOVBE_H
#include <stdint.h>
/* Runs the movbe in `code` (at address `rip`) on `regs` (rax 0 ... r15 15) and its memory:
* its length, or 0 when `code` is no movbe (nothing is changed then). */
int runtime_movbe_run(const uint8_t *code, uint64_t regs[16], uint64_t rip);
/* The SIGILL handler's part: the movbe at *rip run and *rip moved past it (1), or 0. */
int runtime_movbe_fault(uint64_t regs[16], uint64_t *rip);
/* BB_MOVBE_TRAP=file: the listed movbe of the image made ud2 (while it is still writable). */
void runtime_movbe_bind(unsigned char *image, uint64_t size);
/* Whether this processor has MOVBE (CPUID 1, ECX bit 22). */
int runtime_movbe_supported(void);
#endif
+6 -1
View File
@@ -101,7 +101,12 @@ static void sample_host(PadData *d) {
* it) and sample_atlas() gives the game the neutral input. */
if (bbgpu_overlay_captures_input() && !atlas_on()) { apply_mouse_look(d,0,0,d->timestamp,0); return; }
const bool *k=SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetKeyboardState(NULL) : NULL;
mouse_buttons=captured && SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetMouseState(NULL,NULL) : 0;
/* In a game's pointer menu (the mouse released) the buttons count too, but the left one, which
* is the pointer's click (gpu/games/bloodborne_menu_pointer.cpp): the right one is L2 there as
* the game's prompts show it (the storage box's pages, #44). */
const int pointer_menu=!captured && bbgpu_pointer_wanted();
mouse_buttons=(captured || pointer_menu) && SDL_WasInit(SDL_INIT_VIDEO) ? SDL_GetMouseState(NULL,NULL) : 0;
if (!captured) mouse_buttons&=~SDL_BUTTON_LMASK;
const int pad_only=steam_pad_only(g) || !keyboard_enabled();
atomic_store_explicit(&steam_only_now,pad_only,memory_order_relaxed);
if (pad_only) {
+9
View File
@@ -214,8 +214,17 @@ static ABI int32_t save_umount(const MountPoint *point) {
int slot=slot_of(point);
if (slot>=0) { runtime_file_unmount(point->data); slots[slot].used=0; }
pthread_mutex_unlock(&lock);
if (slot>=0) runtime_quit_on_save_done();
return slot>=0 ? 0 : ERR_NOT_FOUND;
}
/* Save data mounted now (Exit to desktop waits for the last unmount). */
int runtime_savedata_mounted(void) {
pthread_mutex_lock(&lock);
int used=0;
for (int i=0;i<SLOTS;++i) used+=slots[i].used;
pthread_mutex_unlock(&lock);
return used;
}
static ABI int32_t save_set_param(const MountPoint *point, uint32_t type, const void *buffer, uint64_t size) {
if (!initialized) return ERR_NOT_INITIALIZED;
if (!buffer) return ERR_PARAMETER;
+47
View File
@@ -17,6 +17,7 @@
#include <pthread.h>
#include <time.h>
#include <arpa/inet.h>
#include <SDL3/SDL_events.h>
#define USER_ID 1
#define ORBIS_OK 0
@@ -132,6 +133,52 @@ static ABI int32_t system_status(unsigned char *status) {
static ABI int32_t system_event(void *event) { (void)event; return SYSTEM_NO_EVENT; }
static ABI int32_t hide_splash(void) { note("SystemService: splash screen hidden"); return 0; }
static ABI int32_t launch_browser(void) { note("SystemService: web browser request ignored (offline)"); return 0; }
/* Exit to desktop in the world (gpu/shim/bbport_game_menu.cpp): the System menu's entry runs the
* game's Exit Game, which saves and leaves the world, and arms the quit here. The game closes
* once it is back at the title: when the intro logos start loading (a menu/logo_ file opens), the
* title menu is built (its texts) or the game's menu flow says its title menu is up (online play
* comes back without either: gpu/games/bloodborne_menu_pointer.cpp, #46; all three call
* runtime_quit_at_title), with no save data mounted, or after
* the last save unmounts. The game's own Exit Game disarms it, and so do two minutes without
* getting there (the question answered No). Closing is the window's own close (SDL_EVENT_QUIT:
* the window thread ends the process with exit code 0). */
#define QUIT_ARMED_SECONDS 120
static int quit_waiting;
static time_t quit_armed;
static pthread_mutex_t quit_lock=PTHREAD_MUTEX_INITIALIZER;
static void quit_game(void) {
note("Exit to desktop: the game is saved and back at the title, closing");
fflush(stdout);
SDL_Event quit={.type=SDL_EVENT_QUIT};
if (!SDL_PushEvent(&quit)) printf("Runtime: Exit to desktop: %s\n",SDL_GetError());
}
static int quit_ready(void) { return quit_armed && time(NULL)-quit_armed<QUIT_ARMED_SECONDS; }
void runtime_quit_arm(int on) {
pthread_mutex_lock(&quit_lock);
if (on || quit_armed || quit_waiting) note(on ? "Exit to desktop: armed (Exit Game saves first)" : "Exit to desktop: disarmed");
__atomic_store_n(&quit_armed,on ? time(NULL) : 0,__ATOMIC_RELAXED);
quit_waiting=0;
pthread_mutex_unlock(&quit_lock);
}
static void quit_at_title(void) {
if (!__atomic_load_n(&quit_armed,__ATOMIC_RELAXED)) return; /* the usual case: one load */
pthread_mutex_lock(&quit_lock);
if (quit_ready()) {
__atomic_store_n(&quit_armed,0,__ATOMIC_RELAXED);
/* A save still writing finishes first (runtime_quit_on_save_done). */
if (runtime_savedata_mounted()) quit_waiting=1; else quit_game();
}
pthread_mutex_unlock(&quit_lock);
}
void runtime_quit_at_title(void) { quit_at_title(); }
void runtime_quit_on_open(const char *guest) {
if (guest && strstr(guest,"/menu/logo_")) quit_at_title();
}
void runtime_quit_on_save_done(void) {
pthread_mutex_lock(&quit_lock);
if (quit_waiting && !runtime_savedata_mounted()) { quit_waiting=0; quit_game(); }
pthread_mutex_unlock(&quit_lock);
}
/* ---- NetCtl / Net / Http / Ssl: no network ---- */
static int32_t net_errno;
+1
View File
@@ -10,6 +10,7 @@ int32_t runtime_guest_errno(int e) { return e; }
void runtime_memory_note_write(uintptr_t address, uint64_t size) { (void)address; (void)size; }
void runtime_memory_note_cpu_write(uintptr_t address, uint64_t size) { (void)address; (void)size; }
void runtime_memory_prepare_cpu_write(uintptr_t address, uint64_t size) { (void)address; (void)size; }
void runtime_quit_on_open(const char *guest) { (void)guest; }
uintptr_t runtime_lookup(const RuntimeExport *table,size_t n,const char *name) {
for (size_t i=0;i<n;++i) if (!strcmp(table[i].name,name)) return (uintptr_t)table[i].function;
return 0;
+200
View File
@@ -0,0 +1,200 @@
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
// When a mirror is made again over its valid blocks only (gpu/layer/layer_mirror_policy.h).
//
// The default run of 0.7 without any BB_MIRROR_* variable compacted nothing for its whole length:
// 223-225 mirrors of 1,750 MiB, 7.8 MiB each, with 612 MiB of waste between them (2.7 MiB each).
// The fixed 8 MiB a mirror had to waste was more than a whole mirror of that run, so no mirror was
// ever eligible. These tests pin the per-mirror share that replaced it, and the segments a mirror
// comes back as.
#include "layer/layer_mirror_policy.h"
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <vector>
namespace {
using VideoCore::LayerPolicy::CompactBudget;
using VideoCore::LayerPolicy::kMaxSegmentsPerMirror;
using VideoCore::LayerPolicy::kRetrySeconds;
using VideoCore::LayerPolicy::kSegmentPays;
using VideoCore::LayerPolicy::MirrorMinHole;
using VideoCore::LayerPolicy::MirrorMinWaste;
using VideoCore::LayerPolicy::MirrorSegments;
using VideoCore::LayerPolicy::Segment;
using VideoCore::LayerPolicy::SegmentsBytes;
constexpr u64 kShift = 16; // the layer's block: 64 KiB
constexpr u64 kBlocksPerMiB = 16; // 64 KiB blocks in a MiB
void TestMinWasteFollowsSize() {
// Never below 1 MiB: a copy costs what it frees otherwise.
assert(MirrorMinWaste(0) == 1_MB);
assert(MirrorMinWaste(1_MB) == 1_MB);
assert(MirrorMinWaste(4_MB) == 1_MB);
// A quarter of the mirror while that is small enough.
assert(MirrorMinWaste(8_MB) == 2_MB);
assert(MirrorMinWaste(16_MB) == 4_MB);
// 31 MiB / 4 is 7.75 MiB, still under the 8 MiB cap.
assert(MirrorMinWaste(31_MB) == 31_MB / 4);
// And at most 8 MiB, the whole per-mirror threshold of 0.7.
assert(MirrorMinWaste(32_MB) == 8_MB);
assert(MirrorMinWaste(1_GB) == 8_MB);
// The run that exposed it: 223 mirrors of 1,750 MiB hold 612 MiB of waste, 7.85 MiB and
// 2.74 MiB a mirror. Under the fixed 8 MiB none qualified; under the share all of them do.
const u64 mirror = 7'850'000; // 7.85 MiB
const u64 waste = 2'870'000; // 2.74 MiB
assert(MirrorMinWaste(mirror) < waste);
assert(waste < 8_MB); // what the old fixed threshold asked for
}
void TestMinHoleFollowsSize() {
assert(MirrorMinHole(1_MB) == 256_KB);
assert(MirrorMinHole(8_MB) == 256_KB);
assert(MirrorMinHole(64_MB) == 2_MB);
assert(MirrorMinHole(1_GB) == 4_MB);
}
void TestNoRuns() {
assert(MirrorSegments({}, kShift, 1_MB, kMaxSegmentsPerMirror).empty());
}
void TestOneRunStaysOneSegment() {
const std::vector<Segment> runs{{0, 100}};
const auto segments = MirrorSegments(runs, kShift, 1_MB, kMaxSegmentsPerMirror);
assert(segments.size() == 1);
assert(segments[0].first == 0 && segments[0].end == 100);
}
void TestHeadAndTailComeOffForFree() {
// Valid [16, 64) in a mirror of 128 blocks (8 MiB): a head of 1 MiB and a tail of 4 MiB. Both
// come off without an extra mirror, so all 5 MiB come back whatever the per-hole minimum says.
const std::vector<Segment> runs{{16, 64}};
for (const u64 min_hole : {u64{256_KB}, u64{2_MB}, u64{4_MB}}) {
const auto segments = MirrorSegments(runs, kShift, min_hole, kMaxSegmentsPerMirror);
assert(segments.size() == 1);
assert(segments[0].first == 16 && segments[0].end == 64);
assert((128 << kShift) - SegmentsBytes(segments, kShift) == 5_MB);
}
}
void TestHoleIsSplitOnlyWhenItPays() {
// An 8 MiB mirror with two 64-block runs and a hole between them.
const std::vector<Segment> two{{0, 64}, {72, 128}}; // the hole is 8 blocks = 512 KiB
// The hole is under the extra mirror's worth (kSegmentPays): kept, so one segment and the
// caller frees nothing beyond the ends.
const auto kept = MirrorSegments(two, kShift, 256_KB, kMaxSegmentsPerMirror);
assert(kept.size() == 1);
assert(kept[0].first == 0 && kept[0].end == 128);
assert(SegmentsBytes(kept, kShift) == 128 << kShift);
// A 2 MiB hole does pay for the mirror it costs: split in two.
const std::vector<Segment> wide{{0, 64}, {96, 160}}; // the hole is 32 blocks = 2 MiB
const auto split = MirrorSegments(wide, kShift, 256_KB, kMaxSegmentsPerMirror);
assert(split.size() == 2);
assert(split[0].first == 0 && split[0].end == 64);
assert(split[1].first == 96 && split[1].end == 160);
assert(SegmentsBytes(split, kShift) == (128 << kShift));
assert((160 << kShift) - SegmentsBytes(split, kShift) == 2_MB);
}
void TestWasteBetweenValidBlocksFreesNothing() {
// Many small holes: the mirror comes back whole, so the caller skips it. This is the case the
// old `continue` decided for every mirror.
std::vector<Segment> runs;
u64 at = 0;
for (u32 i = 0; i < 40; ++i) {
runs.push_back({at, at + 16});
at += 16 + 1; // a 64 KiB hole after each 1 MiB run
}
const u64 size = runs.back().end << kShift;
const auto segments = MirrorSegments(runs, kShift, MirrorMinHole(size), kMaxSegmentsPerMirror);
assert(SegmentsBytes(segments, kShift) == size);
}
void TestSegmentCapKeepsTheSmallestHoles() {
// Twenty runs of 64 blocks with holes of 1..19 MiB between them.
std::vector<Segment> runs;
std::vector<u64> holes;
u64 at = 0;
for (u32 i = 1; i <= 20; ++i) {
runs.push_back({at, at + 64});
holes.push_back(u64(i) * kBlocksPerMiB);
at += 64 + u64(i) * kBlocksPerMiB;
}
holes.pop_back(); // 19 holes between 20 runs
const auto segments = MirrorSegments(runs, kShift, 256_KB, kMaxSegmentsPerMirror);
assert(segments.size() == kMaxSegmentsPerMirror);
// Nothing is lost: every valid block is inside one of the new mirrors, and they do not
// overlap.
std::size_t next = 0;
for (const auto& segment : segments) {
assert(segment.first < segment.end);
while (next < runs.size() && runs[next].first >= segment.first &&
runs[next].end <= segment.end) {
++next;
}
}
assert(next == runs.size());
for (std::size_t i = 0; i + 1 < segments.size(); ++i) {
assert(segments[i].end <= segments[i + 1].first);
}
assert(segments.front().first == runs.front().first);
assert(segments.back().end == runs.back().end);
// Each split is a hole, and the holes split are the biggest ones: with 8 pieces at most, 7
// splits remain and they go to the 7 biggest holes (13..19 MiB).
std::vector<u64> split_holes;
for (std::size_t i = 0; i + 1 < segments.size(); ++i) {
split_holes.push_back(segments[i + 1].first - segments[i].end);
}
const std::vector<u64> expected(holes.end() - 7, holes.end());
assert(split_holes == expected);
}
void TestRunsAreNotSplitForNothing() {
// The guard against the opposite failure (0.7 made 411 mirrors in 8 minutes for 0.4 GB of data
// on the 2 GB laptop): holes of a handful of blocks never pay for the mirrors they would add,
// so the runs stay in one segment.
const std::vector<Segment> runs{{0, 1}, {2, 3}, {4, 6}};
for (const u32 limit : {kMaxSegmentsPerMirror, 1u, 0u}) {
const auto segments = MirrorSegments(runs, kShift, 0, limit);
assert(segments.size() == 1);
assert(segments[0].first == 0 && segments[0].end == 6);
// Nothing freed between the valid blocks: only the (absent) head and tail come off.
assert(SegmentsBytes(segments, kShift) == (6 << kShift));
}
}
void TestRetryAndBudget() {
assert(kRetrySeconds == 30);
// The per-split minimum the two tests above rely on.
assert(kSegmentPays == 1_MB);
// A small waste is taken whole; a large one is capped so the copies spread over the seconds
// that follow instead of landing in one long frame.
assert(CompactBudget(1_MB) == 1_MB);
assert(CompactBudget(32_MB) == 32_MB);
assert(CompactBudget(612_MB) == 32_MB);
assert(CompactBudget(0) == 0);
}
} // namespace
int main() {
TestMinWasteFollowsSize();
TestMinHoleFollowsSize();
TestNoRuns();
TestOneRunStaysOneSegment();
TestHeadAndTailComeOffForFree();
TestHoleIsSplitOnlyWhenItPays();
TestWasteBetweenValidBlocksFreesNothing();
TestSegmentCapKeepsTheSmallestHoles();
TestRunsAreNotSplitForNothing();
TestRetryAndBudget();
std::printf("layer-mirror-policy-test: all checks passed\n");
return 0;
}
+96
View File
@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
/* runtime_movbe.c: the movbe forms the game uses, run on registers and a buffer. */
#include "runtime_movbe.h"
#include <stdio.h>
#include <string.h>
static int failures;
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
++failures; \
} \
} while (0)
enum { RAX, RCX, RDX, RBX, RSP, RBP, RSI, RDI, R8, R9, R10, R11, R12, R13, R14, R15 };
int main(void) {
uint8_t memory[64];
uint64_t regs[16];
const uint64_t base = (uint64_t)(uintptr_t)memory;
/* movbe eax, [rcx+4] (0f 38 f0 41 04): the game's DCX header read (issue #39). */
{
static const uint8_t code[] = {0x0f, 0x38, 0xf0, 0x41, 0x04};
memset(regs, 0, sizeof(regs));
memcpy(memory + 4, "\x00\x01\x10\x00", 4);
regs[RCX] = base;
regs[RAX] = 0xffffffffffffffffull;
CHECK(runtime_movbe_run(code, regs, 0) == 5);
CHECK(regs[RAX] == 0x00011000); /* upper half cleared */
}
/* movbe [rax+r12*4-0x10], r9d (46 0f 38 f1 4c a0 f0): a store, REX.R and REX.X, disp8. */
{
static const uint8_t code[] = {0x46, 0x0f, 0x38, 0xf1, 0x4c, 0xa0, 0xf0};
memset(regs, 0, sizeof(regs));
memset(memory, 0, sizeof(memory));
regs[RAX] = base + 0x10;
regs[R12] = 2;
regs[R9] = 0xaabbccdd11223344ull;
CHECK(runtime_movbe_run(code, regs, 0) == 7);
CHECK(memcmp(memory + 8, "\x11\x22\x33\x44", 4) == 0); /* big-endian */
CHECK(regs[R9] == 0xaabbccdd11223344ull); /* the source is left as it was */
}
/* movbe rdx, [r13+0x100] (49 0f 38 f0 95 00 01 00 00): 64-bit, REX.B, disp32. */
{
static const uint8_t code[] = {0x49, 0x0f, 0x38, 0xf0, 0x95, 0x00, 0x01, 0x00, 0x00};
memset(regs, 0, sizeof(regs));
memcpy(memory + 8, "\x01\x02\x03\x04\x05\x06\x07\x08", 8);
regs[R13] = base + 8 - 0x100;
CHECK(runtime_movbe_run(code, regs, 0) == 9);
CHECK(regs[RDX] == 0x0102030405060708ull);
}
/* movbe [rsp+8], ecx (0f 38 f1 4c 24 08): rsp base through a SIB with no index. */
{
static const uint8_t code[] = {0x0f, 0x38, 0xf1, 0x4c, 0x24, 0x08};
memset(regs, 0, sizeof(regs));
memset(memory, 0, sizeof(memory));
regs[RSP] = base;
regs[RCX] = 0x12345678;
CHECK(runtime_movbe_run(code, regs, 0) == 6);
CHECK(memcmp(memory + 8, "\x12\x34\x56\x78", 4) == 0);
}
/* movbe esi, [rbp+0] (0f 38 f0 75 00): rbp needs a displacement byte. */
{
static const uint8_t code[] = {0x0f, 0x38, 0xf0, 0x75, 0x00};
memset(regs, 0, sizeof(regs));
memcpy(memory, "\xde\xad\xbe\xef", 4);
regs[RBP] = base;
CHECK(runtime_movbe_run(code, regs, 0) == 5);
CHECK(regs[RSI] == 0xdeadbeef);
}
/* movbe ax, [rip+0x10] (66 0f 38 f0 05 10 00 00 00): 16 bits keep the rest, rip-relative. */
{
static const uint8_t code[] = {0x66, 0x0f, 0x38, 0xf0, 0x05, 0x10, 0x00, 0x00, 0x00};
memset(regs, 0, sizeof(regs));
memcpy(memory + 0x20, "\xab\xcd", 2);
regs[RAX] = 0x1111111111111111ull;
const uint64_t rip = base + 0x20 - 0x10 - sizeof(code);
CHECK(runtime_movbe_run(code, regs, rip) == 9);
CHECK(regs[RAX] == 0x111111111111abcdull);
}
/* Not a movbe: nothing is run. */
{
static const uint8_t crc32[] = {0xf2, 0x0f, 0x38, 0xf1, 0xc1};
static const uint8_t ud2[] = {0x0f, 0x0b};
static const uint8_t reg_form[] = {0x0f, 0x38, 0xf0, 0xc1};
memset(regs, 0, sizeof(regs));
CHECK(runtime_movbe_run(crc32, regs, 0) == 0);
CHECK(runtime_movbe_run(ud2, regs, 0) == 0);
CHECK(runtime_movbe_run(reg_form, regs, 0) == 0);
}
printf("movbe: %s\n", failures ? "FAILED" : "all passed");
return failures != 0;
}
+287
View File
@@ -0,0 +1,287 @@
# SPDX-FileCopyrightText: Copyright 2026 bbport contributors
# SPDX-License-Identifier: GPL-2.0-or-later
"""CPU sampler-GC regression with production lookup, Touch and natural age advancement.
Compile the Sampler declaration, lookup/GC bodies, LRU, defer/pop and timeline
accessors. Only Vulkan allocation, image GC and completion progress are adapters.
The 4000-object allocation limit is modeled; no device or game is used.
"""
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import tempfile
import unittest
from paths import ROOT
def body(text, anchor):
start = text.index('{', text.index(anchor))
depth = 0
for at in range(start, len(text)):
if text[at] == '{':
depth += 1
elif text[at] == '}':
depth -= 1
if depth == 0:
return text[start + 1:at]
raise ValueError('unbalanced source: ' + anchor)
VK_STUB = r'''#pragma once
#include <cstdint>
#include <memory>
#include <vector>
namespace vk {
using Sampler=std::uint64_t;
inline unsigned destroyed=0;
inline Sampler next_id=0;
inline std::vector<Sampler> destroyed_ids;
struct UniqueSampler {
std::unique_ptr<Sampler> value;
UniqueSampler()=default;
explicit UniqueSampler(Sampler id):value(std::make_unique<Sampler>(id)){}
UniqueSampler(UniqueSampler&&) noexcept=default;
UniqueSampler& operator=(UniqueSampler&&) noexcept=default;
~UniqueSampler(){if(value){++destroyed;destroyed_ids.push_back(*value);}}
Sampler operator*() const{return *value;}
};
}
'''
HARNESS = r'''#include "common/hash.h"
#include "common/lru_cache.h"
#include "common/scope_exit.h"
#include "common/unique_function.h"
#include "textures/sampler.h"
#include <algorithm>
#include <atomic>
#include <bit>
#include <cstdio>
#include <deque>
#include <mutex>
#include <stdexcept>
#include <string>
#include <tsl/robin_map.h>
#include <xxhash.h>
void check(bool condition, const char *message) {
if (!condition)
throw std::runtime_error(message);
}
namespace Vulkan {
class Instance {};
} // namespace Vulkan
namespace BbToggle {
constexpr int PendingPollLimit = 0;
bool Disabled(int) { return true; }
} // namespace BbToggle
VideoCore::Sampler::Sampler(const Vulkan::Instance &, const Gcn::Sampler &, Gcn::BorderColorBuffer,
bool, float) {
auto created = vk::UniqueSampler(++vk::next_id);
<<OWNER>>
}
VideoCore::Sampler::~Sampler() = default;
struct Timeline {
std::atomic<u64> current_tick{1}, gpu_tick{0};
u64 CurrentTick() const noexcept { <<CURRENT>> }
u64 KnownGpuTick() const noexcept { <<KNOWN>> }
bool IsFree(u64 tick) const noexcept { <<FREE>> }
u64 NextTick() noexcept { <<NEXT>> }
void Refresh() {}
};
struct Scheduler {
Timeline work_semaphore;
struct PendingOp {
Common::UniqueFunction<void> callback;
u64 gpu_tick;
};
std::deque<PendingOp> pending_ops;
std::recursive_mutex pending_ops_mutex;
std::atomic<u32> num_pending_ops{0};
u32 pending_polls = 0;
u64 CurrentTick() const { return work_semaphore.CurrentTick(); }
void DeferOperation(Common::UniqueFunction<void> &&func) { <<DEFER>> }
void PopPendingOperations() { <<POP>> }
void Complete(u64 tick) {
work_semaphore.gpu_tick = tick;
PopPendingOperations();
}
};
struct Cache {
Vulkan::Instance instance;
Scheduler scheduler;
std::mutex samplers_mutex;
tsl::robin_map<u64, VideoCore::Sampler> samplers;
Common::LeastRecentlyUsedCache<u64, u64> sampler_lru;
<<INITIAL_AGE>>
u64 trigger_gc_samplers, pressure_gc_samplers, critical_gc_samplers;
explicit Cache(u32 max_samplers) { <<THRESHOLDS>> }
vk::Sampler GetSampler(const Gcn::Sampler &sampler, Gcn::BorderColorBuffer border_color_base,
bool is_depth, float extra_lod_bias) {
<<GET>>
}
void GarbageCollectImages() {} // no images in this sampler-only fixture
void RunGarbageCollector() { <<RUN_GC>> }
void GarbageCollectSamplers() { <<GC>> }
// Only progress/completion is modeled. Age advances exclusively inside production RunGC.
void CompletedFrame() {
RunGarbageCollector();
scheduler.Complete(scheduler.work_semaphore.NextTick());
}
vk::Sampler Lookup(u64 key) {
Gcn::Sampler sampler{};
sampler.min_lod = u32(key);
sampler.max_lod = 4095;
return GetSampler(sampler, Gcn::BorderColorBuffer{}, false, 0.f);
}
void Fill() {
for (u64 key = 0; key < trigger_gc_samplers; ++key)
Lookup(key);
}
};
int main(int argc, char **argv) {
try {
check(argc == 3, "scenario max-samplers");
const std::string scenario = argv[1];
const auto limit = u32(std::stoul(argv[2]));
check(limit >= 32, "limit too small");
Cache cache(limit);
if (scenario != "startup")
for (int i = 0; i < 20; ++i)
cache.CompletedFrame();
const u64 inserted_age = cache.gc_tick;
cache.Fill();
const auto target = cache.Lookup(0); // calls production Touch even on repeat lookup
const auto initial_count = cache.samplers.size();
if (scenario == "startup" || scenario == "warm-fresh") {
cache.RunGarbageCollector();
const auto early = vk::destroyed;
const bool target_destroyed =
std::find(vk::destroyed_ids.begin(), vk::destroyed_ids.end(), target) !=
vk::destroyed_ids.end();
std::printf("scenario=%s limit=%u trigger=%llu inserted_age=%llu age_after=%llu "
"current_tick=%llu completed_tick=%llu early=%u target_destroyed=%d "
"remaining=%zu pending=%u\n",
argv[1], limit, (unsigned long long)cache.trigger_gc_samplers,
(unsigned long long)inserted_age, (unsigned long long)cache.gc_tick,
(unsigned long long)cache.scheduler.CurrentTick(),
(unsigned long long)cache.scheduler.work_semaphore.KnownGpuTick(), early,
target_destroyed, cache.samplers.size(),
cache.scheduler.num_pending_ops.load());
if (scenario == "startup") {
check(early == 0 && !target_destroyed &&
cache.samplers.size() == initial_count - 10 &&
cache.scheduler.num_pending_ops == 10,
"premature first-GC destruction");
cache.scheduler.Complete(cache.scheduler.work_semaphore.NextTick());
check(vk::destroyed == 10 && cache.scheduler.num_pending_ops == 0,
"startup final release");
} else
check(early == 0 && cache.samplers.size() == initial_count,
"fresh Touch did not protect");
} else {
// The first reference completes before later frames: the default pacing case.
cache.scheduler.Complete(cache.scheduler.work_semaphore.NextTick());
for (int frame = 0; frame < 18; ++frame) {
if (scenario == "touch-hot")
check(cache.Lookup(0) == target, "hot object replaced");
cache.CompletedFrame();
}
bool target_destroyed = std::find(vk::destroyed_ids.begin(), vk::destroyed_ids.end(),
target) != vk::destroyed_ids.end();
check(vk::destroyed == 10, "cold entries did not age out");
check(target_destroyed == (scenario == "paced-cold"), "Touch/expiry result wrong");
std::printf("scenario=%s inserted_age=%llu final_age=%llu destroyed=%u "
"target_destroyed=%d completed_before_expiry=1 pending=%u\n",
argv[1], (unsigned long long)inserted_age,
(unsigned long long)cache.gc_tick, vk::destroyed, target_destroyed,
cache.scheduler.num_pending_ops.load());
}
return 0;
} catch (const std::exception &e) {
std::fprintf(stderr, "FAIL %s\n", e.what());
return 1;
}
}
'''
class SamplerGcTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shlex.split(os.environ.get('CXX', 'c++'))
if not compiler or not shutil.which(compiler[0]):
raise unittest.SkipTest('a C++20 compiler is required')
flags = shlex.split(os.environ.get('CXXFLAGS', ''))
cls.tmp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.tmp.cleanup)
folder = Path(cls.tmp.name)
probe = folder / 'dependencies.cpp'
probe.write_text('#include <tsl/robin_map.h>\n#include <xxhash.h>\n'
'#include <fmt/format.h>\n'
'int main() { return int(XXH3_64bits("", 0) == 0); }\n')
result = subprocess.run(compiler + flags + ['-std=c++20', str(probe),
'-lxxhash', '-o', str(folder / 'dependencies')],
capture_output=True, text=True, timeout=60)
if result.returncode:
raise unittest.SkipTest('C++20, robin-map/fmt headers and libxxhash are required')
vk_header = folder / 'vulkan/vk_common.h'
vk_header.parent.mkdir()
vk_header.write_text(VK_STUB)
read = lambda name: (ROOT / 'gpu' / name).read_text(encoding='utf-8')
texture = read('textures/texture_cache.cpp')
memory = read('textures/texture_cache_memory.cpp')
timeline = read('vulkan/timeline.h')
replacements = {
'OWNER': next(line for line in read('textures/sampler.cpp').splitlines()
if 'handle =' in line),
'CURRENT': body(timeline, 'u64 CurrentTick()'),
'KNOWN': body(timeline, 'u64 KnownGpuTick()'),
'FREE': body(timeline, 'bool IsFree('),
'NEXT': body(timeline, 'u64 NextTick()'),
'DEFER': body(read('render/scheduler.h'), 'void DeferOperation('),
'POP': body(read('render/scheduler.cpp'), 'void Scheduler::PopPendingOperations('),
'INITIAL_AGE': next(line.strip() for line in read('textures/texture_cache.h').splitlines()
if 'u64 gc_tick =' in line),
'THRESHOLDS': '\n'.join(line for line in texture.splitlines()
if '_gc_samplers = max_samplers' in line),
'GET': body(texture, 'vk::Sampler TextureCache::GetSampler('),
'RUN_GC': body(memory, 'void TextureCache::RunGarbageCollector('),
'GC': body(memory, 'void TextureCache::GarbageCollectSamplers('),
}
code = HARNESS
for key, value in replacements.items():
code = code.replace('<<' + key + '>>', value)
source = folder / 'sampler-gc.cpp'
source.write_text(code)
cls.exe = folder / 'sampler-gc'
result = subprocess.run(compiler + flags + ['-std=c++20', '-Wall', '-Wextra', '-Werror',
'-I', str(folder), '-I', str(ROOT / 'gpu'), str(source),
'-lxxhash', '-pthread', '-o', str(cls.exe)],
capture_output=True, text=True, timeout=60)
if result.returncode:
raise RuntimeError(result.stdout + result.stderr)
def run_case(self, mode):
result = subprocess.run([str(self.exe), mode, '4000'], capture_output=True,
text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
print(result.stdout, end='')
def test_first_gc_retains_fresh_samplers_until_render_completion(self):
self.run_case('startup')
def test_fresh_lookup_after_warmup_does_not_evict(self):
self.run_case('warm-fresh')
def test_completed_cold_samplers_age_out(self):
self.run_case('paced-cold')
def test_repeated_lookup_refreshes_the_hot_sampler(self):
self.run_case('touch-hot')
if __name__ == '__main__':
unittest.main()
+408
View File
@@ -0,0 +1,408 @@
# SPDX-FileCopyrightText: Copyright 2026 bbport contributors
# SPDX-License-Identifier: GPL-2.0-or-later
"""A queued submission must not be visible to the decoder before its count is published.
Compiles the actual SubmitGfx/SubmitAsc publication blocks and the consumer's pop
block from gpu/cp/command_processor.cpp. The producer is paused at its publication
point (after the queue push, before the counter); the queue is then inspected the
way the decoder does it, under the queue mutex. If the task is visible there, the
count must already cover it.
Why the pause is injected instead of taking submit_mutex: the decoder takes the
same mutex inside its pop block, so parking the producer on it would park the
consumer too -- which is exactly why the window is short in production. The pause
sits on the boundary under test; the queue, both mutexes and the counter are real.
No Vulkan device and no command decoding: only the queue, the mutexes and the
counter take part, which is where the ordering lives.
Also executes the production SignalGpuIdle and WaitGpuIdle bodies. Rasterizer
and GPU execution are omitted: idle callbacks are explicitly invoked by the test.
"""
import os
from pathlib import Path
import re
import shlex
import shutil
import subprocess
import tempfile
import unittest
from paths import ROOT
SOURCE = ROOT / 'gpu/cp/command_processor.cpp'
PUSH = 'queue.works.push_back(std::move(work));'
def _block_start(text, pos):
"""Index of the line holding the lone '{' that opens the block around `pos`."""
opener = None
for match in re.finditer(r'\n([ \t]*)\{\n', text[:pos]):
opener = match
if opener is None:
raise ValueError('no block opener before offset %d' % pos)
return opener.start() + 1
def _match_brace(text, start):
depth = 0
for index in range(start, len(text)):
if text[index] == '{':
depth += 1
elif text[index] == '}':
depth -= 1
if depth == 0:
return index + 1
raise ValueError('unbalanced block at offset %d' % start)
def _producer_block(text, function):
"""One producer: the queue push under the queue mutex, then the count publication."""
entry = text.index('void CommandProcessor::%s' % function)
push = text.index(PUSH, entry)
start = _block_start(text, push)
end = text.index('submit_cv.notify_one();', push) + len('submit_cv.notify_one();')
block = text[start:end]
block = block.replace(PUSH, PUSH + '\n NotePushed();')
# Pause on the boundary under test: before the count's lock, wherever it is.
return re.sub(r'([ \t]*)(std::scoped_lock \w+\{submit_mutex\};)',
r'\1HoldPublication();\n\1\2', block)
def _pop_block(text):
entry = text.index('void CommandProcessor::Run(')
pop = text.index('queue.works.pop_front();', entry)
start = _block_start(text, pop)
return text[start:_match_brace(text, start)]
def _statement(text, start_anchor, statement):
entry = text.index(start_anchor)
at = text.index(statement, entry)
return text[at:at + len(statement)]
def _body(text, anchor):
entry = text.index(anchor)
start = text.index('{', entry)
return text[start + 1:_match_brace(text, start) - 1]
def _idle_snapshot(text):
entry = text.index('void CommandProcessor::AfterBatch(')
at = text.index('decoded = num_submits == 0;', entry)
start = _block_start(text, at)
return text[start:_match_brace(text, start)]
HARNESS = r'''#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <deque>
#include <latch>
#include <functional>
#include <string>
#include <memory>
#include <mutex>
#include <span>
#include <thread>
#include <utility>
using u32 = std::uint32_t;
using u64 = std::uint64_t;
constexpr u64 NoSeq = ~u64{0};
namespace BbStats {
std::atomic<u64> guest_submit_stamp{0};
}
namespace BbTimeline {
inline constexpr int GuestSubmit = 0;
inline void Note(int, u64 = 0, u64 = 0) {}
}
namespace BbFreeCheck {
inline void NoteSubmit(u64, const u32*, size_t) {}
}
struct SubmittedCopy {
const u32* guest_dcb = nullptr;
};
struct Prep {
void Enqueue(u64, void*) {}
void* Build(std::span<const u32>) { return nullptr; }
};
struct Work {
std::span<const u32> dcb, ccb;
u64 seq = NoSeq;
u64 stamp = 0;
std::shared_ptr<SubmittedCopy> copy;
};
namespace BbIrq {
enum class Line { GpuIdle };
std::atomic<unsigned> raises{0};
void Raise(Line) { ++raises; }
}
// A diagnostic wrapper around a real mutex. While the producer is paused, the
// flag lets the observer distinguish a held queue lock without try_lock's
// permitted spurious failure. Lock acquisition/release order is unchanged.
struct TrackedMutex {
std::mutex mutex;
std::atomic<bool> held{false};
void lock() { mutex.lock(); held.store(true); }
void unlock() { held.store(false); mutex.unlock(); }
};
struct State {
struct Queue {
TrackedMutex mutex;
std::deque<std::unique_ptr<Work>> works;
} queue;
std::mutex submit_mutex;
std::condition_variable submit_cv;
std::atomic<u32> num_submits{0};
std::atomic<u32> num_commands{0};
u64 submissions_total = 0;
u64 decoded_total = 0;
u32 num_mapped_queues = 1;
u32 gnm_vqid = 1;
u64 gfx_submit_seq = 0;
std::atomic<bool> work_retired{true};
Prep* draw_prep = nullptr;
std::span<const u32> dcb, ccb;
Work* last_decoded = nullptr;
std::deque<std::pair<u64, u64>> frame_ends;
bool frame_end = false;
std::atomic<bool> armed{false};
std::latch pushed{1};
std::latch pub{1};
std::latch pub_release{1};
void NotePushed() {
if (armed.load(std::memory_order_acquire)) {
pushed.count_down();
}
}
void HoldPublication() {
if (!armed.load(std::memory_order_acquire)) {
return;
}
pub.count_down();
pub_release.wait();
}
void Submit(bool gfx) {
auto work = std::make_unique<Work>();
auto* draw_prep = this->draw_prep;
[[maybe_unused]] void* prepared = nullptr;
if (gfx) {
<<GFX>>
} else {
<<ASC>>
}
}
void SignalGpuIdle(u64 generation) {
<<SIGNAL>>
}
std::function<void()> IdleCallback() {
u64 generation;
bool decoded;
<<SNAPSHOT>>
(void)decoded;
return <<CALLBACK>>;
}
std::latch waiting{1};
std::atomic<bool> wait_noted{false};
void NoteWaitPredicate() {
if (num_submits != 0 && !wait_noted.exchange(true)) waiting.count_down();
}
void WaitGpuIdle() {
<<WAIT>>
}
void PopOne() {
<<POP>>
}
bool Empty() {
std::scoped_lock lock{queue.mutex};
return queue.works.empty();
}
/// AfterBatch's own decision, on the same counter.
bool WouldBeIdle() {
bool decoded = false;
<<IDLE>>
return decoded;
}
/// Run()'s loop condition, on the same counters.
bool LoopContinues() {
return (<<LOOP>>);
}
};
struct Result {
bool visible = false;
u32 counted = 0;
u32 queued = 0;
bool premature = false;
u32 left = 0;
u64 total = 0;
unsigned early_idle = 0;
unsigned stale_idle = 0;
unsigned fresh_idle = 0;
};
Result Run(bool gfx) {
State s;
s.Submit(gfx); // the counted one
s.armed.store(true, std::memory_order_release);
std::thread producer([&] { s.Submit(gfx); }); // pushed, not yet counted
s.pushed.wait();
s.pub.wait();
Result r;
{
r.visible = !s.queue.mutex.held.load();
if (r.visible) {
std::scoped_lock q{s.queue.mutex};
r.counted = s.num_submits.load();
r.queued = u32(s.queue.works.size());
}
}
if (r.visible) {
s.PopOne();
r.premature = s.WouldBeIdle() && !s.LoopContinues();
}
auto idle = s.IdleCallback();
BbIrq::raises = 0;
idle();
r.early_idle = BbIrq::raises.exchange(0);
s.pub_release.count_down();
producer.join();
while (!s.Empty()) {
s.PopOne();
}
// The callback captured before the new publication must now be stale even
// after all work is drained: the generation guard must reject it.
idle();
r.stale_idle = BbIrq::raises.exchange(0);
s.IdleCallback()();
r.fresh_idle = BbIrq::raises.exchange(0);
r.left = s.num_submits.load();
r.total = s.submissions_total;
return r;
}
int main(int argc, char** argv) {
const std::string mode = argc > 1 ? argv[1] : "publication";
if (mode == "wait") {
State s;
s.Submit(true);
std::atomic<bool> returned{false};
std::thread waiter([&] { s.WaitGpuIdle(); returned = true; });
s.waiting.wait(); // actual wait predicate observed outstanding work
const bool early = returned.load();
s.PopOne(); // production queue -> submit pop, then notify_all
waiter.join();
std::printf("wait: early=%d returned=%d count=%u\n", int(early),
int(returned.load()), s.num_submits.load());
return early || !returned || s.num_submits != 0 ? 1 : 0;
}
int bad = 0;
for (int i = 0; i < 2; ++i) {
const bool gfx = i == 0;
const Result r = Run(gfx);
if (r.visible) {
std::printf("%s: snapshot=available counted=%u queued=%u premature_idle=%d left=%u total=%llu\n",
gfx ? "SubmitGfx" : "SubmitAsc", r.counted, r.queued,
int(r.premature), r.left, static_cast<unsigned long long>(r.total));
} else {
std::printf("%s: queue_locked snapshot=not_taken left=%u total=%llu\n",
gfx ? "SubmitGfx" : "SubmitAsc", r.left,
static_cast<unsigned long long>(r.total));
}
if (mode == "publication" && r.visible) {
std::printf(" a queued submission was visible before its count was published\n");
++bad;
}
if (r.premature) {
std::printf(" the decoder leaves the loop with a submission still queued\n");
}
std::printf(" idle callbacks: before_publication=%u stale=%u fresh=%u\n",
r.early_idle, r.stale_idle, r.fresh_idle);
if (mode == "early-idle" && r.early_idle != 0) ++bad;
if (mode == "generation" && (r.stale_idle != 0 || r.fresh_idle != 1)) ++bad;
if (r.left != 0 || r.total != 2) {
std::printf(" the count and the queue do not agree after the drain\n");
++bad;
}
}
return bad == 0 ? 0 : 1;
}
'''
class SubmissionPublishTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shlex.split(os.environ.get('CXX', 'c++'))
if not compiler or not shutil.which(compiler[0]):
raise unittest.SkipTest('a C++20 compiler is required')
text = SOURCE.read_text(encoding='utf-8')
callback = re.search(r'rasterizer->SignalAfterGpu\((\[this, generation\] \{ SignalGpuIdle\(generation\); \})\);', text)
if callback is None:
raise ValueError('idle callback anchor changed')
wait = _body(text, 'void CommandProcessor::WaitGpuIdle(')
wait = wait.replace('return num_submits == 0;',
'NoteWaitPredicate(); return num_submits == 0;')
code = (HARNESS
.replace('<<SIGNAL>>', _body(text, 'void CommandProcessor::SignalGpuIdle('))
.replace('<<SNAPSHOT>>', _idle_snapshot(text))
.replace('<<CALLBACK>>', callback.group(1))
.replace('<<WAIT>>', wait)
.replace('<<GFX>>', _producer_block(text, 'SubmitGfx'))
.replace('<<ASC>>', _producer_block(text, 'SubmitAsc'))
.replace('<<POP>>', _pop_block(text))
.replace('<<IDLE>>', _statement(text, 'void CommandProcessor::AfterBatch(',
'decoded = num_submits == 0;'))
.replace('<<LOOP>>', _statement(text, 'void CommandProcessor::Run(',
'while (num_submits || num_commands) {')
[len('while ('):]
[:-len(') {')]))
cls.tmp = tempfile.TemporaryDirectory()
cls.addClassCleanup(cls.tmp.cleanup)
folder = Path(cls.tmp.name)
source = folder / 'publish.cpp'
source.write_text(code, encoding='utf-8')
cls.exe = folder / 'publish-test'
result = subprocess.run(compiler + ['-std=c++20', '-Wall', '-Wextra', '-Werror',
'-pthread', str(source), '-o', str(cls.exe)],
capture_output=True, text=True, timeout=120)
if result.returncode:
raise RuntimeError(result.stdout + result.stderr)
def run_case(self, mode):
result = subprocess.run([str(self.exe), mode], capture_output=True, text=True, timeout=15)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
print(result.stdout, end='')
def test_count_is_published_before_the_task_is_visible(self):
self.run_case('publication')
def test_idle_callback_cannot_accept_unpublished_queued_work(self):
self.run_case('early-idle')
def test_generation_rejects_stale_callback_and_accepts_fresh_idle(self):
self.run_case('generation')
def test_wait_gpu_idle_blocks_then_wakes_after_the_real_pop(self):
self.run_case('wait')
if __name__ == '__main__':
unittest.main()
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""The image offsets of the movbe in the game's code, one per line, for BB_MOVBE_TRAP
(src/runtime_movbe.c): a game run with them made ud2 tests the port's movbe on any processor.
python3 tools/movbe_sites.py out/eboot.elf > out/movbe-sites.txt
BB_MOVBE_TRAP=out/movbe-sites.txt bash run.sh
Needs objdump (binutils)."""
import struct
import subprocess
import sys
import tempfile
def code_segments(elf):
phoff, = struct.unpack_from('<Q', elf, 0x20)
phentsize, phnum = struct.unpack_from('<HH', elf, 0x36)
for i in range(phnum):
kind, flags, offset, vaddr, _, filesz = struct.unpack_from('<IIQQQQ', elf, phoff + i * phentsize)
if kind == 1 and flags & 1:
yield vaddr, elf[offset:offset + filesz]
def main():
elf = open(sys.argv[1], 'rb').read()
for vaddr, code in code_segments(elf):
with tempfile.NamedTemporaryFile() as f:
f.write(code)
f.flush()
listing = subprocess.run(
['objdump', '-D', '--insn-width=16', '-b', 'binary', '-m', 'i386:x86-64',
f'--adjust-vma={vaddr:#x}', f.name], check=True, capture_output=True, text=True).stdout
for line in listing.splitlines():
fields = line.split('\t')
if len(fields) >= 3 and fields[2].startswith('movbe'):
print(fields[0].strip().rstrip(':'))
if __name__ == '__main__':
main()