Compare commits

..
142 Commits
Author SHA1 Message Date
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
ayozetr 74b1a30870 README: what the unlocked frame rate allows
The features list said the unlocked frame rate went "up to 120 FPS". In
0.7 it goes up to 480 FPS, the most the game's loading screens take, and
120 is the most the game's movement is sure to keep up with (the
launcher warns above it). The line now says so, and that the in-game
menu changes the frame rate while playing.
2026-11-03 21:38:26 +00:00
ayozetr 62dbeefa6b AppImage build: the new image is renamed into dist/
packaging/appimage-in-arch.sh copied the new AppImage over
dist/Bloodborne-bbport-x86_64.AppImage. When the previous build was still
open (its launcher, or the game), that changed the file under its mount
and the running program could crash. The new image is now copied next to
it and renamed over it: the open one keeps running from the old file,
and the next start uses the new one.
2026-11-03 21:20:06 +00:00
7c6b25a560 Keyboard: the Linux fork's movement keys by default
bbport 0.7 turns the movement keys into a stick that ramps up over 70 ms,
turns at 1100 degrees a second and keeps a diagonal within a round gate,
and opposite keys held together cancel out. In the game that stops a
sprint when A and D (or W and S) overlap for a moment, and a diagonal
gives 90 of 127 on each axis while the game picks walk or run by the
larger one.

The movement keys now work as in 0.51-linux: the one of two opposite
keys pressed last wins, a diagonal stays on the square, the direction
turns toward the keys' at 900 degrees a second and at most 30 degrees a
read (a half turn going through forward), and a released key holds its
direction for 60 ms, so the game never sees a turn of 45 degrees or a
neutral read that would end a sprint (cassioKenji's measurements in
Supermedo/bloodborne_pc#74). bbport.ini keyboard_movement=0.7 gives back
0.7's stick; stick_socd=neutral and stick_circular=1 work as before. The
walk key and the camera keys are unchanged. The pad tests cover the keys'
direction, the turn, the release hold and the settings; the README says
it, and Jhony-Jhansen and cassioKenji are back in the credits and the
About page's thanks (in the seven languages).

Co-Authored-By: João Pedro Valadares <[email protected]>
Co-Authored-By: Supermedo <[email protected]>
2026-11-03 21:19:29 +00:00
ayozetr 43ffa68201 Launcher: About page layout
The About page lists this project, the players' guide, support, the
licence and the thanks first, and the related projects' links in one
expandable row after them, with its title in the seven languages.
2026-11-03 21:17:15 +00:00
ayozetr 0f90125d4a Discord: the repository fits in the art's hover text
Discord shows the large image's text in a narrow box that wraps at spaces
only. "github.com/ayozetr/bloodborne_linux" was one word wider than the
box and came out cut ("github.com/ayozetr/bloodbor"). The text is now
"Bloodborne on Linux · ayozetr/bloodborne_linux on GitHub": the
repository has a line of its own and the "Get the Linux port" button
still opens it. The test checks that no word is longer than the box.
2026-11-03 21:12:17 +00:00
ayozetr 02dc648c96 Build release AppImages in a plain Arch Linux container
packaging/appimage-sharun.sh bundles the system libraries of the PC it
runs on. Built on CachyOS, those come from its x86-64-v3 repositories:
libpython, GLib and GTK then carry AVX2/BMI2 instructions outside any
CPU check, and the AppImage died with SIGILL before showing anything on
processors without them (AMD FX and Phenom, Intel before Haswell; issue
#39).

packaging/appimage-in-arch.sh clones the current commit with its
submodules into out/arch-build/, builds the image from
packaging/appimage-arch.Dockerfile (archlinux:base-devel plus the build
and packaging dependencies, VulkanMemoryAllocator 3.5.0 and xbyak 7.43
headers from git), and runs build.sh, tools/build_dlss.sh and
packaging/appimage-sharun.sh inside it as the calling user. The DLSS SDK
cache and fsr4_shaders/ already on the PC are reused. The image keeps the translations Arch's Docker image leaves
out (GTK's and libadwaita's, X11's compose tables) and has the ALSA plugins for
PipeWire and PulseAudio, as a desktop does. Host networking is
used for both the image build and the run.

Checked on the result: libpython, GLib and libstdc++ have no BMI2
instructions; the ones left in glibc, GTK (CRoaring), FFmpeg, OpenSSL,
GMP and the codecs sit behind runtime CPU dispatch, as in Arch itself.
The README names the script as the way releases are built.
2026-11-03 02:01:38 +00:00
ayozetr c16b6d3d58 Discord Rich Presence: the repository in the image's tooltip and a button to it
Whoever sees the status in Discord can find the port: the large image's tooltip
names github.com/ayozetr/bloodborne_linux, and a "Get the Linux port" button
opens it. Discord shows the button to the others looking at the profile, not to
the player. Tested against Discord's length limits.
2026-11-01 23:44:35 +00:00
ayozetr b4f75ec15c Credits: what this build actually takes from each one
The credits and the launcher's thanks listed work that did not come over to 0.7:
features 0.7 replaces (the direct mouse camera, ultrawide and the HUD position,
Quit to Desktop, bbbridge, DLSS's groundwork - 0.7's DLSS is its own), the
earlier online module's shadNet fixes, keyboard movement and sprint, cloth, plants
and effects at high frame rates (pending a test against 0.7's), and renderer
fixes 0.7 does differently. Each entry now names what is in this tree; entries
with nothing left go (MaskZeus, Jhony-Jhansen, mcrib884, HatsyRei, pmwork-personal,
cassioKenji, jpramosjp, devilismyfriend, Obrikash, Vorshim92, AwesomeObserver,
gibs2193, zFenrys, and Abken's mod 537), in the credits and the launcher's thanks
in every language. Work that comes back later brings its credit back with it.
2026-11-01 23:38:23 +00:00
ayozetr c8373033ad Icon: a hunter before the crimson moon
A new icon for the launcher, the AppImage's desktop entry and the Steam shortcut:
the hunter's silhouette before a blood moon over Yharnam's spires, in a red rounded
frame. 256 px with its corners transparent (it was 128 px), stripped of metadata;
it reads at 24 px too. The old one (launcher/bloodborne.svg and its PNG) goes, and
so does it from Supermedo's credit, in the credits and the launcher's thanks in
every language. A Steam shortcut takes the new icon the next time Add to Steam is
used.
2026-11-01 23:33:10 +00:00
ayozetr 42e8b4c606 Gestures menu without the motion-control icons, as an option again
The Linux fork offered Dommo's stripped-down gestures menu (Nexus Mods,
Bloodborne mod 30) as a switch on the Controls page; it went with the fork's
prompt layer when 0.7's live prompts took over. It is back:
scripts/gestures_menu.py applies assets/menu_prompts/gesturetop.json to the
player's own menu/gesturetop.gfx (only to the file the edits were made for) as a
layer under the mods, when BB_GESTURES_NO_MOTION=1, the launcher's switch, keeps
the same setting it had. Checked against a retail 1.09 dump: the edits apply.
2026-11-01 23:31:27 +00:00
ayozetr afac1a4f39 Launcher: an own server at Seamless Hunt's address keeps its address
0.7's default shadNet address is Seamless Hunt's, and the Online page left the
own-server field empty when it held the default. The Linux fork's guide had
players enter Seamless Hunt as their own server, so for them the field showed
empty, and saving the page would have lost the server. With an own server
chosen the field now always shows its address.
2026-11-01 23:23:52 +00:00
ayozetr 4c1e2e3061 Launcher: BBSC's settings again, folded under its switch
The Linux fork's launcher had a page of BBSC's options; on 0.7 only the switch
was left, and the options could be changed through extra variables only. They
are back as an expander, "BBSC settings", under the switch: what the guest
shares with the host, five hunters, enemy scaling and its percentages per guest,
shared progress and the rest, 28 options from BBSC 0.0.2's list
(scripts/bbsc_options.json) with their names in the launcher's seven
languages. Only the options changed from BBSC's defaults are kept and passed to
the game (scripts/bbsc.py, through online.environment). Left out: BBSC's two
master switches (the BBSC switch is the one), its own Discord status and its P2P
channel, which 0.7's BBSC does not read.
2026-11-01 23:23:52 +00:00
ayozetr ab3088469a Docs: online and environment pages match 0.7's online module
- ONLINE.md quoted log lines of the earlier online module and said the co-op port
  is random by default. 0.7 uses UDP 3658, the PS4's own, unless another is set (the
  launcher's 0 keeps it), and logs its own lines for the router, the port, a sign-in
  and a server that refuses the build; the page quotes those now. The launcher's
  comment on the setting says the same.
- ENVIRONMENT.md: its list regenerated (BB_STEAM_INPUT_X11, BB_MOTION_LARGE and the
  others added since), and its header says which part is generated.
- The design notes (memory module, translator, parallel GPU commands) say they
  predate 0.7 and name switches that no longer exist.
2026-11-01 17:02:04 +00:00
ayozetr dbc374fcec run.sh: an explicit render size above 1080p gets the direct memory it needs
0.7 gives a scene above 1080p more direct memory on its scaled path only. A render
size set by hand (BB_RENDER_RES) skips that path while patches.py still grows the
graphics heaps for it, and the game stopped at start (exit 139); the Linux fork's
run.sh handled it. It does again, through the same large_scene_memory.
2026-11-01 17:00:38 +00:00
ayozetr 8e9dbc20e1 Game menu: 0.7's new rows in every language, and the test that checks them
The rows 0.7 adds to the game's own menu (the Display page with aspect ratio,
resolution and texture quality, FSR 4 through AMD's DLL, Exit to desktop, Bloom
among the effects) showed in English in Spanish, Italian, French, German,
Portuguese and Chinese. The Linux fork's test of the menu's translations, which
caught them, comes back with them.
2026-11-01 17:00:19 +00:00
ayozetr 027e834a79 Steam overlay in the sharun AppImage: found above the launcher, and passed on
0.7 loads Steam's overlay when its packaged entry (the Nix AppImage) names it in
BB_STEAM_OVERLAY. The Linux fork's AppImage is built with sharun, which has no such
entry and clears LD_PRELOAD in every program of the package, so the overlay never
loaded although the launcher's switch is on by default (#28). When Steam started the
game and nothing named it, run.sh reads Steam's 64-bit gameoverlayrenderer.so from
the nearest process above that still preloads it, as the fork's run.sh did; with
the overlay it asks sharun to pass LD_PRELOAD on (SHARUN_ALLOW_LD_PRELOAD=1) and, on
a Wayland desktop, opens the window through Xwayland, where Steam can draw it.
Tested with a simulated process tree; BB_STEAM_OVERLAY=0 still turns it off.
2026-11-01 16:59:04 +00:00
ayozetr 0219c34163 AppImage: the assets folder again, with the Xbox and Switch button art
scripts/prompt_art.py makes the live button prompts' Xbox and Switch icons from
assets/menu_prompts, but the AppImage no longer carried that folder: the prompts fell
back to letters without a word. It is copied again, as the Linux fork's AppImage did.
2026-11-01 16:58:17 +00:00
ayozetr f0306a62a1 Content: a dump without a region gets Europe (#9)
A dump whose param.sfo has no region (USER_DEFINED_PARAM_1 = 0) gave the game no
address for its server, and Log In failed with "Unable to obtain Bloodborne server
information". The Linux fork gave such a dump Europe; that went out with its Region
row, which 0.7's text and voice languages replace. It is back, without the row, and
tested.
2026-11-01 16:58:17 +00:00
ayozetr 5fd9e2ccb5 Repository: BBSC's text tables count as C++ in GitHub's language bar
The Linux fork marked BBSC's table of game texts as C++, since its <?name?>
placeholders made GitHub take it for PHP. The table moved to gpu/bbnet20/bbsc in
0.7; the attribute follows it, and covers its translations too.
2026-11-01 16:50:34 +00:00
ayozetr 311aeb36ff DLSS: tools/build_dlss.sh back, with Frame Generation, and the AppImage carries both
The Linux fork's releases ship DLSS ready to use: tools/build_dlss.sh fetches the
files of NVIDIA's DLSS SDK 310.9.1 once into ~/.cache/bbport, builds the bridge
and puts it with NVIDIA's library next to bb-probe, and the AppImage takes them.
The script had not come over to 0.7, so a build without it asked the player for
the library. It is back, and it also fetches the DLSS Frame Generation library,
which 0.7 uses on GeForce RTX 40 and newer; the AppImage now copies that one too.
2026-11-01 16:50:28 +00:00
ayozetr 3963108624 Steam Input: the window through Xwayland when Steam made a virtual controller (#12)
Steam sends its virtual controller's input only while it sees the game's window
focused, and on a Wayland desktop it only sees X11 windows: started from Steam
with Steam Input on, the controller did nothing. The Linux fork's run.sh opened
the window through Xwayland then, and that block did not come over to 0.7; it is
back (BB_STEAM_INPUT_X11=0 keeps Wayland). docs/TROUBLESHOOTING.md already
describes it.
2026-11-01 16:49:48 +00:00
ayozetr afef07c90f Version 0.70-linux.1, first in every log with bbport's
The launcher's version (launcher/bbport_update.py) moves to 0.70-linux.1, the
first release on bbport 0.7. run.sh prints it first in every log, with the build
and the bbport version it is built on: "bbport Linux 0.70-linux.1, build ...
(bbport 0.7)"; without the launcher's file, as before.
2026-11-01 15:31:37 +00:00
ayozetr aae3f0566f Credits: bbport 0.7's contributors, and the projects as 0.7 uses them
The projects table follows what this tree takes from each one now: the online
module is shadPS4 0.20's network libraries put together by Awaken, with
droogie's bbhost behind The Hunter's Dream backend, and BBSC lives in
gpu/bbnet20/bbsc with its game server in scripts/bbsc_gameserver; shadp2p and
bbbridge are no longer part of it (MaskZeus keeps the credit for bbbridge in
earlier releases). A new section names everyone bbport 0.7 took work from, with
a link to bbport's own list.
2026-11-01 15:30:55 +00:00
ayozetr dff4079ef6 Docs: BBPORT.md is bbport 0.7's README
BBPORT.md kept the original project's README from bbport 0.3. It is now 0.7's,
still marked as the original project's, with its links to documents this
repository does not carry pointing at deadinside28/bloodborne_pc.
2026-11-01 15:30:22 +00:00
ayozetr e0543751fb Docs: the environment variables from the port's own registry
docs/ENVIRONMENT.md described the 0.51 tree by hand, with file and line
references that no longer exist. Its list is now the one gpu/shim/bb_env_vars.def
gives (scripts/env_vars.py --doc), which tests/test_env_vars.py keeps complete;
how to set them and their precedence stay written by hand, brought up to date.
2026-11-01 15:30:22 +00:00
ayozetrandMikeTat1988 76ebf406f3 Hunter's Atlas: its map generator, running on Linux
The launcher's Map page (launcher/bbport_atlas_maps.py) prepares the
maps with Atlas's generator in tools/hunters-atlas, which this branch did
not have: Prepare had nothing to run. It is the Linux fork's copy of the
scripts of Atlas's source/generator/src at 4ee4a0ed (Atlas 1.3) by
MikeTat1988, with:

- art/: Atlas's source/art, its PNGs recompressed without loss (the same
  pixels, checked against their hashes by the launcher), and the fallback
  font with its licence;
- requirements.lock: exact versions with their hashes, installed by the
  launcher with uv into atlas-tools/ in the data folder;
- localize_names.py: the names of areas, lamps and pickups in the game's
  language;
- the Linux fix for parameter tables looked up by name (soulstruct gives
  Windows paths, which Path.name does not split on Linux).

The AppImage carries it next to the launcher, as in the Linux fork.
tests/test_atlas_maps.py covers the launcher's side (the maps' state,
the tools' reuse, the downloads' checks, the shipped art and lock) and
passes. scripts/env_vars.py skips BB_PATH, Soulstruct's game folder
setting in the generator, not an environment variable.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:27:21 +00:00
ayozetrandMikeTat1988 c27d8fb36b Hunter's Atlas: the map keeps what it covers when the window size changes
The map's zoom is in pixels of the drawn map, so a window made larger or
smaller (a resize, fullscreen, a new resolution) showed more or less of
the area than before. The zoom now remembers the UI scale it was set for
and follows that scale in Draw; Fit's limits scale with it too. From
Hunter's Atlas 3.0, as the Linux fork has it.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:25:56 +00:00
ayozetrandMikeTat1988 2719bce35f Hunter's Atlas: travel dialog selection and Alt+Tab
As in the Linux fork:
- The travel dialog marks its selected button and Left/Right choose
  between Travel and Cancel; the confirm button does what is selected.
  Only the button under the mouse was marked, so with a controller
  neither looked chosen.
- Losing the window's focus (Alt+Tab, local co-op) no longer closes the
  map; only what is held (sticks, zoom) is let go.
- After a travel the quick-menu tile stays hidden until the game's menu
  has closed, instead of showing on the loading screen.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:24:19 +00:00
ayozetrandMikeTat1988 8cfcdc1f76 Hunter's Atlas: the Map tile follows the game's quick menu
Measured frame by frame against recordings of the game's own menu, as in
the Linux fork:
- the tile fades in and slides with the menu as it opens, instead of
  appearing 180 ms late and still;
- it stays until the menu leaves the screen, fades out with it when the
  menu is closed with Back, and is gone before a lamp travel's loading
  screen;
- losing the window's focus (Alt+Tab, local co-op) no longer hides it or
  drops its selection;
- back from the System page the game's menu manager reports 0 instead of
  2 while the menu is shown; 0 with the quick menu's list alive counts as
  shown (src/runtime_map.c already reads it so), so the tile no longer
  stays hidden until the menu is reopened;
- its "Map" label has the game's size, colour and weight, and the cover
  over the game's own label is only as wide as that label, opaque and
  faded at its edges, without hiding the divider line;
- Up from below the Map tile focuses it and Down from it reaches the tile
  below it, as for a native tile: the game knows of no tile above the
  fourth column, so Up landed on System and Down went below System (the
  game is moved to the tile below System and Right is pulsed until it
  gets there). tests/test_bbport_atlas_menu_state.cpp covers both ways.

BB_ATLAS_DEBUG=1 (docs/ENVIRONMENT.md) logs what the tile reads from the
quick menu whenever it changes, which is how the System case was found.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:22:35 +00:00
ayozetrandMikeTat1988 a68b6a75c8 Hunter's Atlas: the map and its tile in the game's language
Atlas had its texts in English, with a Russian label on the tile only.
They now follow the port's other texts in the game, as in the Linux fork:

- the map and the quick-menu tile follow the game's language, like the
  port's pages in the game's System menu (BbGameMenu::Language, now
  exported: the game's own once its menu has looked up a text, else the
  port's menu language);
- the map's section of the port's menu (Insert) follows that menu's
  language;
- gpu/shim/bbport_atlas_i18n.h holds them in Russian, Spanish, Italian,
  French, German, Brazilian Portuguese and Simplified Chinese, with the
  game's own names where it has them (Farol, Fiole de sang, Озарение);
- the map's titles use the game's own map font in Russian too (it has
  Cyrillic); only Chinese takes the port's font, the map font having no
  CJK glyphs.

Identifiers stay in English (the travel popup's id); the text the footer
compares is translated on both sides. The names of areas and lamps come
from the generator. tests/test_atlas_i18n.cpp checks the columns and the
English fallback and runs in build.sh --test.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:20:17 +00:00
ayozetrandMikeTat1988 2bdee3185a Hunter's Atlas: the map screen, its quick-menu tile and input
Second part of Hunter's Atlas by MikeTat1988 (GPL-2.0-or-later,
github.com/MikeTat1988/HuntersAtlas): the fullscreen map, drawn with the
overlay's ImGui, and the "Map" tile next to the game's quick menu, as the
Linux fork has them.

- gpu/shim/bbport_map*.cpp, bbport_atlas_*: the map screen (layers,
  floors, walls, lamps, markers, travel dialog), its art, features, tile
  and controller shortcut. Their BB_* variables are read through bb_env()
  and listed in bb_env_vars.def (BB_MAP_SOUND_GAIN and the map's test
  hooks are new there). The map packs are read with nlohmann/json, the
  system package the online module already builds with.
- The overlay draws and ticks them, routes events and the pad to them,
  loads the map's title font and has the map's section in its menu.
  MenuOpen() is also true while the map or the tile uses the pointer, so
  the window lets the mouse go and shows the system cursor over them, as
  for the menu; CapturesInput() also covers the open map. The L3+R3 hold
  that opens the menu is unchanged.
- runtime_pad/sample.c: with the map on, the pad is read even while the
  map holds it, Atlas takes what it uses (bbgpu_atlas_route_pad), the game
  gets neutral input meanwhile, and buttons still held afterwards stay
  hidden until released (sample_atlas). Scripted input, the injected
  mouse and the game modules' pad filter stay on that path.
- runtime_audio.c and runtime_menu_sound.c: the tile's cursor sound,
  decoded on first use from the player's own sound/sprj_main.fsb with
  LibAtrac9 and mixed into the main port at the level of the game's own
  quick-menu cursor (CURSOL_SELECT, BB_MAP_SOUND_GAIN), silent in the
  background like the game.

Everything is behind BB_ATLAS_ENABLED=1: off, the overlay calls none of
it and the pad keeps its current path. build.sh --test runs the pad with
the map on (test_pad_atlas.c), the tile's state machine, map styles,
floor interiors, the controller shortcut and the features on a synthetic
features.json; test_menu_sound.c needs the player's own sound bank and
stays a manual test.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:17:04 +00:00
ayozetrandMikeTat1988 6621a1f0a7 Hunter's Atlas: the game-state readers and lamp travel
First part of Hunter's Atlas by MikeTat1988, a fullscreen in-game map
(GPL-2.0-or-later, github.com/MikeTat1988/HuntersAtlas), as the Linux fork
has it: the runtime pieces that read the game's state.

- src/runtime_map.c: the player's position, facing and area, the quick
  menu's state and which button confirms. Reads only; every guarded site
  is checked byte for byte first.
- src/runtime_atlas_state.c: event flags (read only) and the opt-in lamp
  travel (BB_MAP_TRAVEL_ENABLED=1), which hooks the game's frame pacer
  entry (0x2034770, not a site 0.7's frame rate hooks touch) and calls the
  game's own return-point and warp functions on the game thread.
- bb-probe binds them right after bbgpu_patch_image, and only with
  BB_ATLAS_ENABLED=1: with the map off, nothing of the game is touched.

Both test programs run in build.sh --test and pass. The map screen comes
next.

Co-Authored-By: MikeTat1988 <[email protected]>
2026-11-01 15:04:28 +00:00
ayozetr e2ac43b725 Local co-op: each player's window in its part of the screen, without the keyboard
The launcher's Local co-op page starts one game per player through X11
with BB_WINDOW_RECT, BB_KEYBOARD=0 for the controller players and
BB_PAD_BACKGROUND=1; 0.7 already took the chosen controller strictly
(BB_GAMEPAD_STRICT), kept it in the background and chose the monitor
(BB_DISPLAY). Now:

- BB_WINDOW_RECT=x,y,w,h (fractions of the display, read with a point
  whatever the locale) places a borderless window in that part of the
  screen, never fullscreen.
- BB_KEYBOARD=0: the game ignores the keyboard and the mouse, which reach
  whichever window has the focus; its player has a gamepad. The window
  drops their events as it does for Steam's gamepad-only mode.
- BB_PAD_BACKGROUND=1: an unfocused player's window keeps its sound
  (players 2-4 are silenced by BB_AUDIO=none instead).

From the Linux fork.
2026-11-01 15:02:38 +00:00
ayozetrandAwaken 614602cf36 Faults: a read or write fault stays within its page
A write or read fault on the last bytes of a mapping invalidated or read 8
bytes past its end: IsMapped failed and the fault went unhandled (the game
ended). Both now stay within the faulting page. From Awaken's
BloodBorneButPc engine (0.9.24), as the Linux fork has it; that change's
other half (a page whose tracking keeps faulting gets the guest's access
back) is not ported yet.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:59:43 +00:00
ayozetrandAwaken f0068ecb34 VideoOut: a full flip queue no longer refuses an end-of-pipe flip
A full flip queue refused an end-of-pipe flip, which nobody can take back:
its caller asserted (exit 23). It is queued anyway and reported a few
times; a flip the game submits itself is still refused, as on the console.
The out-of-order flip IRQ half of this change was already in 0.7 (it waits
for the label and flips anyway). From Awaken's BloodBorneButPc engine
(0.9.24), as the Linux fork has it.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:58:07 +00:00
ayozetrandAwaken 1f98a632b5 GPU: a garbage descriptor is reported and read as a known one, not exit 23
Several lookups ended the game (an assertion, exit 23) on a value outside
their tables, as a stale or garbage sharp brings: unknown tile, macro tile
and array modes and pipe configs, sample counts and bits per element out
of range, a mode that is not macro tiled (tiling.cpp), a data format with
no size (formats.cpp), a data/number format combination the surface table
lacks and an invalid sample count (gcn_to_vk.cpp). Each is now reported a
few times and replaced by a known value; at worst that surface renders
wrong. From Awaken's BloodBorneButPc engine (0.9.24), as the Linux fork
has it.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:56:49 +00:00
ayozetr 4e0aa911d2 Runtime: large copy lists are recorded in slices
Loading an area on NVIDIA, a download of the buffer cache carried 178563
buffer copies (4.3 MB) into one deferred command. A command's data shares a
128 KiB recording chunk with it, so ReserveRecordData's check stopped the
game (exit 23). Buffer copies, guest chunk copies, buffer-to-image uploads
and image-to-buffer downloads are now recorded as one command per 64 KiB
of regions; UpdateBuffer already split its data. From the Linux fork.
2026-11-01 14:54:33 +00:00
ayozetrandSupermedo 312d119b5a VRAM full: allocate past the budget instead of stopping
On 6 GB cards the game went over the driver's VRAM budget and the next
image or buffer allocation stopped it ("image allocation" / "buffer
allocation", exit 23). An allocation the budget refuses is now made past
it, where the driver may keep it in system memory: slower, but the game
goes on, and the texture collector turns aggressive for a while. Logged
once per kind. Buffers that must be in VRAM or nothing (the mirrors) still
do without, as before.

From Supermedo/bloodborne_pc 44e7644, as the Linux fork has it; its other
half (dropping old GPU-written tiled images unwritten) is left out: the
Linux fork later had to write them back instead (#13, #14).

Co-Authored-By: Supermedo <[email protected]>
2026-11-01 14:53:03 +00:00
ayozetr 0d34da1577 Pipelines: skip the draws of a pipeline the driver cannot create
"Failed to create graphics pipeline: ErrorUnknown" (some AMD and older
drivers) stopped the game with code 23. The pipeline now stays empty and
is logged; its draws and dispatches are skipped, on the GPU thread and when
a background compile finishes, and it is not saved to the cache, so the
next start tries it again. A cached one the driver rejects still rebuilds
the cache, as before. After Supermedo/bloodborne_pc 188c1f6, as the Linux
fork has it.
2026-11-01 14:50:37 +00:00
ayozetrand0xCydral 28eac08195 Pipeline: skip a draw whose shader program address is not readable
A malformed command buffer can leave a shader program address that is not
mapped (0x4c0012d0000 during an area load), and reading the program there
ended the game. LooksLikeProgram, which every stage of every draw and
dispatch goes through, first checks that the program is readable and skips
the draw or dispatch otherwise; the last readable mapping is cached per
thread against the mapping table's generation. From 0xCydral's change in
the Linux fork (b99535b, after yumlevi/bloodborne_pc ce385b6 and
GoncaloLobo0/bloodborne_pc_win ef70ed2).

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:47:55 +00:00
ayozetrand0xCydral f0aa0fa2ae Draw preparation: a failed check or an exception ends the attempt, not the game
Draw-preparation workers specialize shaders ahead of the GPU thread from
descriptors the game may still be rewriting. A fault there already returned
to a recovery point and left the draw to the GPU thread, but a failed check
on such a torn descriptor stopped the port with exit 23, and a C++ exception
from the same data (a container over its capacity, an empty optional)
escaped the worker and ended the process through std::terminate.

A failed check while this thread's recovery point is armed now jumps there
like a fault (common/assert.cpp calls the pipeline cache's hook; anywhere
else the check still stops), and an exception is caught and treated the
same. From 0xCydral's changes in the Linux fork (917d2c0, df619c7).

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:45:22 +00:00
ayozetrandAwaken 983c89fa9a Shaders: the phi pass queues duplicate groups in a fixed order
The phi simplification pass queued its duplicate SCC groups in the hash
table's order, which is not the same from one run to the next, so the same
shader could compile to different SPIR-V each time. They are queued in the
SCCs' own order now (upstream shadPS4 #5364). From Awaken's Lantern Engine
(0.9.37 beta 5), as the Linux fork has it.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:42:17 +00:00
ayozetr da3ff0ecb0 Shaders: per-mip image bindings counted the same everywhere a stage's bindings start
An image with a binding per mip level (DynamicIndex, on GPUs without
VK_AMD_shader_image_load_store_lod, such as NVIDIA's) takes one binding
number per level in the pipeline layout and in the SPIR-V, but
Info::AddBindings and the rasterizer counted one: a stage after it found
its resources at numbers the layout does not have. Both now count as the
layout does (Info::ImageBindingCount; the T# is read only for such
images). A stage whose start moves gets a new permutation, since
StageSpecialization compares the start, so saved pipelines stay valid.
From the Linux fork.
2026-11-01 14:41:16 +00:00
ayozetrandAwaken d698ad290f Shaders: a vertex shader is reused only for the same bindings
Two stage specializations with no guest sharps compared equal, but the
engine's own buffers (user data, clip planes and the like) carry no sharp
and are still bound from `start` on. A vertex shader with only those was
reused after a fragment shader with another binding count, so its bindings
did not match the pipeline layout: NVIDIA reads other descriptors, AMD's
driver can end the process. The shortcut now needs a stage with no
resources at all, and the resource counts must match.

The pipeline key version moves to 6: pipelines saved with such pairs are
rebuilt once (and shader recipe packs of version 5 no longer load; none
ships with the Linux build). Found by Awaken (Lantern Engine 0.9.37 beta 7).

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:39:25 +00:00
ayozetrand0xCydral e5fde7ed25 Log: a performance line every 30 seconds in normal play
Players' logs carried no frame rate, so a report of low FPS or stutter
came with nothing to measure. Without BB_FRAME_STATS the game now prints
one line per 30 seconds: game frames per second, the 1% low, the worst
frame and the shader/pipeline compiles of the window, for example
"Perf: 68.0 FPS over 30 s, 1% low 62.8 FPS, worst frame 22.1 ms, 0
shader/pipeline compiles". From 0xCydral's change in the Linux fork.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:37:09 +00:00
ayozetr 7c4a740338 Presenter: say so when a frame or a swapchain image does not come for 10 s
The presenter's frame fence and the swapchain acquire were waited for
without a limit, so a GPU that never finished a frame, or a compositor
that kept every image, froze the game with nothing from them in the log
(the Linux fork's #5: frozen loading screens, GPU at 0 %, NVIDIA Xid 109).
They are now waited for in 10 s slices: past the first one the log says
which wait it is, once, with the GPU breadcrumbs for a frame, and again
when it does finish. The GPU watchdog already did this for submitted
work.
2026-11-01 14:35:48 +00:00
ayozetrand0xCydral 91f63ff553 Motion vectors: object motion for 16-64 KiB bone palettes (the large beasts)
Palettes of whole bones between 16 and 64 KiB (24576, 49152 bytes: the
large beasts in Central Yharnam) were left to camera vectors and smeared
under FSR. They are now LargeSkeleton: gated like small skeletons (history
only while the palette changes), since instance data of static props may
look the same. BB_MOTION_LARGE=0 restores the old rule. The motion history
test checks the classification and passes. From 0xCydral's change in the
Linux fork, after bmy/bbport-mac 80898cf.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:34:24 +00:00
ayozetrand0xCydral 7331bb9410 Texture cache: the same hash range when an image is marked maybe dirty and when it is refreshed
MarkAsMaybeDirty hashed the whole image but RefreshImage only its first
8x8 texels, so the two never matched: every image a CPU write near it had
marked was uploaded again from the game's memory, over what the GPU had
written there (a black character preview, a missing loading-screen item
picture), and the collector's check before dropping such an image (the
earlier #30 change) could never clear it either. All three now use
MaybeDirtyHash, the whole image. From 0xCydral's change in the Linux fork,
after yumlevi/bloodborne_pc 43fcf95.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:32:46 +00:00
ayozetrandartemkaVG 2399b7a708 Texture cache: a color image at the stencil address is not taken as the stencil
FindDepthTarget linked whatever image sat at the stencil address as the
depth target's stencil. When a color target reuses that memory, reads and
storage writes to it went to the depth buffer through an invalid view.
Only images with a stencil-compatible format (or none yet) are linked now.
From shadps4-emu/shadPS4#5118 by artemkaVG, whose NVIDIA GPU hang (Xid 109)
went away with it, as the Linux fork has it.

Co-Authored-By: artemkaVG <[email protected]>
2026-11-01 14:31:18 +00:00
ayozetrandemurphy04 cefd12304c AvPlayer: a video queue cleared while a frame is taken no longer crashes
GetVideoData peeks at the next frame's time and then pops it; a Clear()
from another thread in between (the player stopping, the game closing)
left nothing to pop, and the empty result was used. It now gives up when
nothing came back, as GetAudioData already does. From emurphy04's change
in the Linux fork.

Co-Authored-By: emurphy04 <[email protected]>
2026-11-01 14:31:18 +00:00
ayozetrand0xCydral e1239378d6 FSR 4: depth barriers name the depth image's own aspects
The vendored FSR 4 backend issued its layout barriers on every external
image with VK_IMAGE_ASPECT_COLOR_BIT, invalid for the depth image
(VUID-VkImageMemoryBarrier-image-03319 and subresourceRange-09601). The new
fsr-vulkan patch 0004 adds an optional aspectMask to
FfxFsr4VkExternalImageState (0 = color), and Register passes the depth's
aspects: depth and stencil, or depth alone for a format without stencil.
From 0xCydral's change in the Linux fork (its patch 0002).

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:29:47 +00:00
ayozetrand0xCydral 7f8cbf12a4 Vulkan: vulkan.hpp is included with NDEBUG everywhere, so the dispatcher's layout matches
vulkan.hpp adds fields to DispatchLoaderBase when NDEBUG is undefined, so
the layout of vk::detail::DispatchLoaderDynamic depends on it. libbbgpu is
built with NDEBUG while the renderer tests build without it (their asserts
must run); the inline init() copies the linker keeps could then read the
dispatcher at offsets another translation unit did not write. It is now
included with NDEBUG defined and assert() restored after. From 0xCydral's
change in the Linux fork; the camera motion test still passes.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:28:19 +00:00
ayozetr 7f69b82128 FSR 4: the balanced model for Performance and Ultra Performance
The v07 performance and ultra performance models leave trails behind moving
characters in Bloodborne; the balanced one handles 2x and 3x cleanly. Those
presets now use it, as IFreemz's shadPS4-Bloodborne-DLSS-FSR (9b2766e) and
the Linux fork do; the render size still follows the preset.
2026-11-01 14:28:19 +00:00
ayozetrand0xCydral edfb811965 Indirect guard: an invalid limit keeps the default
An unparsable BB_INDIRECT_GUARD_MAX or BB_INDIRECT_GUARD_MAX_VERTICES gave
strtoul 0, so the shader skipped every indirect dispatch or draw without a
word. A value that is not a number from 1 to 4294967295 now keeps the
default and prints one warning line. From 0xCydral's change in the Linux
fork, applied to both limits.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:26:13 +00:00
ayozetr 4a6177cf06 Monitor list: the monitor's pixels, not the scaled desktop size
On a scaled Wayland desktop SDL gives the desktop mode in points, so a
2560x1440 monitor at 115 % was listed as 2226x1252 (the launcher's display
choice). The size is now the points times the display's pixel density.
Unscaled monitors list as before.
2026-11-01 14:25:23 +00:00
ayozetrandMrsuss60 a238c6937b Texture cache: FindImage's memo no longer reads a deleted image's slot
FindImage remembers its last result per description, and when the
registrations have changed since, it checks whether the remembered image
still matches: by reading its slot, which by then may belong to an image
deleted meanwhile. The slot is read only while it is allocated. From
Mrsuss60's Windows port, as the Linux fork has it.

Co-Authored-By: Mrsuss60 <[email protected]>
2026-11-01 14:24:54 +00:00
ayozetrandAwaken 1441367174 Indexed indirect draws sync their whole index buffer
An indirect draw's index count is in GPU memory, and NUM_INDICES may be
another draw's smaller count, so indices past it could be read stale: a
possible cause of stretched models (the Linux fork's #25). The buffer's own
size (VGT_DMA_MAX_SIZE) is synced when it is larger, up to 4M indices and
only over mapped memory. From Awaken's Lantern Engine (940396b).

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:24:09 +00:00
ayozetrandAwaken 8c6aeeb1c5 Draws read their vertex inputs again when the prepared ones are stale (#25)
A draw-preparation worker reads a draw's vertex buffer descriptors (V#s)
ahead of the recording thread. When the game rewrites the table in between,
the worker's copy has stale addresses or strides and vertex fetch reads the
wrong data: a stretched shape across the screen, reported on an RTX 4060 in
the Hunter's Dream (the Linux fork's #25). The comparison with the V#s the
draw's packet carries was there, but only for statistics (BB_CE_STATS). A
draw whose prepared V#s differ now reads its inputs from the packet's, as
without a worker, and the log says how many did ("Vertex inputs: ...",
every 5 s at most). Found by Awaken (Lantern Engine 0.9.34).

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:22:34 +00:00
ayozetrand0xCydral ae6c8728f0 Camera motion: keep the camera vector where an object vector strays far from it
Object motion history that was not the object's gave still geometry huge
vectors: at one moment of the Hunter's Dream, bushes behind the workshop
moved 100-650 px a frame while the camera vectors around them were right,
and FSR and frame generation smeared them. The motion merge now keeps the
camera vector when the object vector differs from it by more than 5% of the
render width (at least 32 px). The camera motion test gains a far object
vector that is rejected and a 30 px one that is kept; both pass. From
0xCydral's change in the Linux fork.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 14:21:08 +00:00
ayozetrandAwaken 5c8bf041d7 Camera motion: no NaN, Inf or wild vectors into the upscaler
A camera whose matrices are not finite is not used, and the shader zeroes
NaN/Inf motion and clamps it to four times the render size, so one bad
frame cannot poison the upscaler's history. From Awaken's Lantern Engine
0.9.38, as the Linux fork has it.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 14:21:08 +00:00
ayozetr c93a1f915c Tests: the camera motion test builds and passes again
camera-motion-test (built on its own: cmake --build out/gpu --target
camera-motion-test; it needs a Vulkan GPU) no longer linked, the renderer
library having gained guest memory, heap growth, files and saves services
the test stubs lacked, and its object motion cases failed: the object
motion texture is now two 32-bit words a pixel (x and y as halves, then the
depth with the "written" flag in its top bit, as EmitFragmentMotion packs
it), and the test still wrote four floats. With the stubs added and the
test packing it the same way, its six cases pass again.
2026-11-01 14:21:07 +00:00
ayozetrandnick24051998 b2a3990de2 SRT walker: a null table pointer zeroes its subtree without a fault
A resource table the game leaves null in one draw faulted in the generated
walker, and the fault handler supplied zero for that load. Each such draw
cost a signal (Supermedo's port counted 2068 such loads across 37 shaders
in Ebrietas' area). The walker now checks each table pointer it loads and,
when it is null, zeroes the subtree's slots in the flat buffer itself. A
pointer that is not null but bad still faults and reads zero for that draw.
From nick24051998's change in Supermedo's port (257d6bbf), as the Linux
fork has it.

Co-Authored-By: nick24051998 <[email protected]>
2026-11-01 14:16:44 +00:00
ayozetr 4e715f48d5 Texture collector: no write-back over memory the game reused (#25, #32, #35)
When the collector frees a GPU-written image it writes it back into the
game's memory first. Write tracking does not see every change there: the
game's heap reusing the memory of an area left behind. The image still
looked clean, its write-back overwrote the heap's bookkeeping, and the game
crashed later in its allocator (0x208599f, 0x20859c7, 0x208596a) or stopped
with "memory block seems to be improper or freed already"; on the Steam
Deck at every second lamp travel (the Linux fork's #35). Keeping GPU-written
images longer (the previous change) makes their write-back older, and more
exposed to this.

The game's memory under an image is now hashed when the GPU first writes it
since its last upload (Image::NoteGpuBase, on every path that marks an image
GPU-written: draws, storage writes, copies, blits, resolves, clears and the
scene resolution's copies), and the write-back goes ahead only if it is
unchanged; otherwise the image is dropped. The pressure report counts them.
2026-11-01 14:15:02 +00:00
ayozetr 5b8ac8efa5 Texture collector: GPU-written images are kept until they are really unused (#30)
Two ways the collector lost what the GPU had drawn, which came back black
once the game used the image again (effects in the Hunter's Dream, the
player's armour, on 8 GB cards that are under pressure all session; the
Linux fork's #30):

- Linear GPU-written images were downloaded and freed under mere memory
  pressure. They now go only past the critical mark and unused for a
  minute. BB_GC_KEEP_GPU_WRITTEN=0 restores the old rule.
- An image "maybe touched by the CPU" (a page under it was written) counted
  as dirty, so what the GPU had written was dropped undownloaded. The
  suspicion is now settled by the same hash RefreshImage uses
  (TextureCache::MaybeDirtyHash) before the image is judged.
2026-11-01 14:12:12 +00:00
ayozetr d96d384538 Texture cache: a GPU-written linear image is downloaded with all its mip levels (#30)
DownloadImageMemory copied mip level 0 only. When the collector freed a
GPU-written linear image, its smaller levels never reached the game's
memory, and the image loaded again later had the game's old bytes there:
black wherever those levels were drawn, effects, particles and models seen
from afar (the Linux fork's #30). Every level of a linear image with all
its levels is now copied to its offset in the game's layout; tiled images
and ones without their largest levels keep level 0.
2026-11-01 14:12:12 +00:00
ayozetr 683b593f45 Online: no SIGPIPE when the shadNet server or BBSC's relay goes away mid-send
On Linux, send() on a TCP connection the other side has closed raises
SIGPIPE, whose default ends the process, and nothing in the game ignores
it: a shadNet server or BBSC relay that went away while the game was
sending closed the game instead of letting the worker reconnect. Both now
send with MSG_NOSIGNAL, as the Linux fork's BBSC did; the failure is then
an error the code already handles. vendor-changes.diff records the shadNet
client's part.
2026-11-01 14:08:43 +00:00
ayozetr a0ec0efaf3 Discord Rich Presence: the co-op partners, on shadNet and The Hunter's Dream
The presence helper (scripts/discord_presence.py) shows "Co-op with
<partners>" from BB_PRESENCE_DIR/coop, which the earlier online module
wrote and this one did not, so the status stayed at "Online". Both pools
now write the online IDs of the players they are connected with, only when
they change: on shadNet the worker, which already wakes every 500 ms, reads
the active signaling connections (vendor-changes.diff records it); on The
Hunter's Dream its signaling writes it when a connection is announced, ends
or is reset.
2026-11-01 14:07:26 +00:00
ayozetr 90ba06f7fc Own server: the shadNet and BBSC game server Docker images
docs/SERVER.md walks a group through hosting its own co-op server with
two Docker images that were not in this tree: tools/shadnet-server
(Wozzardman's shadnet-p2p on Debian 13, for Qt 6.8) and tools/bbsc-server
(BBSC's game server, now from scripts/bbsc_gameserver). The guide's paths
follow the game server's new place.
2026-11-01 14:03:32 +00:00
ayozetr 456532bfa7 BBSC game server: BBSC 0.0.2's, as the Linux fork has it
The game server next to the game (run.sh, online.py bbsc-local) was the
one from before BBSC 0.0.2, while the game's BBSC is 0.0.2 with its relay
client. It is now 0.0.2's: relay frames batched per client and routed per
session, and message and bloodstain replies kept under 64 KB, newest
first (larger ones made the game stop with "Dantelion2 Panic ...
Transaction failed"), among the rest. Its README is the Linux fork's. The
options run.sh passes are the same; tested with them: the server list and
a message area request are answered.
2026-11-01 14:03:32 +00:00
ayozetr 59da2f9a6f BBSC kit: the newest save folder, and the one the game will write on a first start
BBSC's save folder was the first SPRJ0005 the user folder listed, which
with saves of two regions could be the old one, and on a first start there
was none, so the co-op kit was never given. It is now the most recently
written one, and on a first start the folder the game will write
(savedata/1/<title id>/SPRJ0005): the runtime exports the saves' title id
as BB_SAVEDATA_TITLE.
2026-11-01 13:59:57 +00:00
ayozetr 47085f688c BBSC kit: its items keep their names with Seamless co-op off
BBSC gives every character five items of its own in goods slots the game
leaves unused and names them in memory while it runs; with Seamless co-op
off, or on The Hunter's Dream (no BBSC), they showed as "?GoodsName?". A
layer under the mods (scripts/bbsc_kit_texts.py) writes their name, BBSC's
caption and a one-line help saying they need BBSC into the game's item
texts in every language, only into empty slots, so no text of the game
changes. The names and captions come from BBSC's translations
(bbsc_kit_texts.json, --update remakes it). The FMG reader and writer join
fromsoft_files.py, with the archive helpers fov.py already uses.
2026-11-01 13:58:12 +00:00
ayozetr 10732fb349 BBSC: its texts in the language the game shows
BBSC writes its messages, item names and notices in English whatever the
game's language. They now follow the language the game shows its text in
(bbport.ini game_text_language, else BB_LANGUAGE) in Spanish, French,
German, Italian, Portuguese and Russian: bbsc/translations/*.json map BBSC's
English text to each language (texts naming one boss through their {boss}
form, numbers through {n} and {count}), gen_translations.py makes them into
core/bbsc_translations.inc, and core/bbsc_i18n.cpp looks them up by the
English text, so BBSC's own sources keep their strings. Every text BBSC
shows in this version has its translation.
2026-11-01 13:56:26 +00:00
ayozetr e4d879ef3c BBSC: enemy scaling and solid NPCs keep looking for the game's tables after 3 minutes
Both searches gave up 3 minutes after the start and never came back, so a
slow first start (shaders compiling on the title screen) left the session
without enemy scaling, or with the host world's NPCs see-through to a
guest. They now keep looking, every 5 s for those 3 minutes and every 15 s
after, as the co-op kit's own search already does.
2026-11-01 13:53:40 +00:00
ayozetr c8bfa7f54c BBSC: the F8 unstuck key on Linux
BBSC's F8 (put the player back on the ground when stuck inside another
player) only existed in its Windows mod: it reads the key with
GetAsyncKeyState and checks that the game's window is in front. On Linux
the key comes from SDL, which the game's window already uses, looked up at
run time, and the window is in front while SDL has one with keyboard focus.
docs/ONLINE.md already lists it.
2026-11-01 13:53:40 +00:00
ayozetr 61824c2b2a BBSC: its logs and captures kept to the newest five, and the kit's bells in its trace
Every game start wrote a new BBSC timeline and a new trace capture, kept
for good, and the capture grew about 30 MB an hour of play. Now the newest
five of each stay (older starts' files go when a new one opens), and a
capture stops at 64 MB.

The trace of the responder bell check kept only the vanilla Small Resonant
and Sinister Resonant Bells (205, 225), which the co-op kit retires; the
Kin and Bloodlust Bells that replace them (4121, 11201) are recorded too, so
a greyed-out Kin Bell leaves something to look at.
2026-11-01 13:53:40 +00:00
ayozetr a9dcce1eac shadNet: keep trying in the background when the server does not answer at start
A server that did not answer when the game started (still starting, a VPN
not up yet, a short outage) left the game offline for the whole run: the
start-up probe disabled shadNet for the session, and a sign-in that failed
was not retried. Both now schedule the background retries a dropped
connection gets (from 2 s, backing off to 30 s). A server that answers and
refuses (wrong name or password, an incompatible protocol) is not retried,
at start or after a drop.

NpHandler::Shutdown() is never called, so an exit() with the network worker
running aborted in std::thread's destructor and hid the exit code; the
destructor now stops the worker and lets it go.

vendor-changes.diff and SHADNET-PORT.md record both.
2026-11-01 13:49:54 +00:00
ayozetr 96bd219320 Port menu from the controller: how long L3+R3 is held, or off (#38)
menu_pad_hold_ms in bbport.ini (800 by default, as upstream's #304) sets how long L3+R3 is held
to open the in-game menu; 0 leaves both sticks to the game and only Insert opens it, for PvP,
where both get pressed by accident. Closing with L3+R3 is unchanged. The launcher's Controls page
has the choice (0.8 s, 2 s, off) in its seven languages.
2026-11-01 13:05:57 +00:00
ayozetr c0cde157e3 Launcher: the Linux fork's launcher on 0.7, with 0.7's new settings
The fork's own launcher (its sidebar, pages and seven languages) replaces
0.7's, wired to 0.7's engine:

- 0.7's settings as rows of its own: the GPU, the aspect ratio and the
  monitor's own output resolution, anti-aliasing, texture quality, GT-VBAO,
  bloom, frame generation with DLSS's frame generation library, FSR 4.1.2's
  models from AMD's driver, low latency (Reflex, Anti-Lag 2), the game's text
  and voice languages, the stick camera's speed and the mouse's horizontal
  invert.
- The button prompts are bbport.ini's button_prompts, drawn live by the game
  (Switch among them); the keyboard's layout is 0.7's, or the fork's classic
  one. Settings from earlier launchers move once: keyboard_layout=bbport
  becomes classic and 0.6 becomes 0.7's, and the prompts go to bbport.ini.
- The Online tab on 0.7's online module: The Hunter's Dream's Discord sign-in
  as 0.7 does it, BBSC as one switch, the P2P port's default 3658.
- The Steam overlay on by default (BB_STEAM_OVERLAY=0 when it is off).
- Gone with 0.7: the memory model and 0.3's synchronisation, the background
  pre-upload, the game region (the text language replaces it), live
  resolution changes and the HUD position (0.7 widens the view and keeps the
  HUD at the edges by itself), the direct mouse camera switch, the Quit to
  Desktop switch (0.7's Exit to desktop is always there), the gestures menu
  without motion icons for now, bbbridge and BBSC's options.
- Hunter's Atlas and local co-op keep their pages; their variables are
  registered.

Tests for the launcher's modules, its settings' migration and frame
generation's rows; the user docs follow.
2026-11-01 11:31:48 +00:00
ayozetr fcca90e6fa Online: the Linux fork's server choice, accounts and checks on 0.7's module
scripts/online.py keeps 0.7's pools (Seamless Hunt with BBSC, The Hunter's
Dream through the online module's own client) and gains what the fork's
launcher builds on: the kind of server (Seamless Hunt, an own one or The
Hunter's Dream) and one account for each, the game's variables for them
(environment; BB_P2P_PORT, BB_BBSC, BB_NET_BACKEND=thd with the Discord
sign-in's token), the connection test with shadNet's UDP ping and the
sign-in, account registration, the firewall's advice, an own server's game
server answering in plain http, and Seamless Hunt's bloodstains and death
visions left out unless BB_SEAMLESSHUNT_TOMBS=1 (they crash the game). The
bbbridge parts are gone: the online module talks to The Hunter's Dream itself.
2026-11-01 11:31:32 +00:00
ayozetr 0fd636c7f0 Button prompts: button_prompts=switch
The Switch's art and labels for any gamepad, as button_prompts=xbox gives the
Xbox's: the Linux fork's launcher offers it with the other choices.
2026-11-01 11:31:24 +00:00
ayozetr 4bac966821 Discord Rich Presence: the area the player is in
scripts/discord_presence.py shows "Playing Bloodborne" on the player's
Discord profile, with the area: the runtime writes the first map of each load
into BB_PRESENCE_DIR (src/runtime_file.c), and run.sh starts the helper next
to the game for each start; it ends with the game. BB_DISCORD=0 (the
launcher's Discord switch) turns it off. From the Linux fork.
2026-11-01 11:31:19 +00:00
ayozetr 32383ee5d6 Install from PKG: pkgextract in the build and the AppImage
scripts/pkg_install.py extracts the player's own PS4 packages (the game, the
1.09 update and the add-ons) into a game folder with pkgextract
(paulomanrique/ps4-pkg-extractor, Unlicense), which build.sh builds with cargo
at a reviewed revision and the AppImage carries (BB_PKGEXTRACT). Without
cargo the launcher asks for a game folder only. From the Linux fork.
2026-11-01 11:31:12 +00:00
ayozetr 8cf70f6492 Mods: the layouts mod sites use, and why a folder is not a mod
A mod found up to three wrapper folders deep (an archive extracted into a
folder of its name, notes without a suffix, previews and links beside it),
under any title ID, and the game's folders next to screenshots or optional
folders, which stay out of the game. unrecognized() names each folder of the
mods folder that is not a mod and why (empty, still an archive, several mods
in one, no game files), for the launcher's list. From the Linux fork; the
extra layouts are from Supermedo's port (dd2314e).
2026-11-01 11:31:03 +00:00
ayozetr 1bd6314b69 Button prompts: the Xbox and Switch art on 0.7's live prompts
0.7 draws the game's key guide icons for the device in use, live: an
Xbox or a Switch pad got letters on a plain badge. They now get the
Linux fork's art, Dommo's "DS3 Style" prompts (Nexus, Bloodborne mod
30; assets/menu_prompts, with its README's credit and terms):

- scripts/prompt_art.py (run.sh, at start) writes each KG_*.dds (BC7,
  32x32) as raw RGBA into out/prompt_art/<xbox|switch>, kept while newer
  than its source; BB_PROMPT_ART names the folder.
- BbPrompts::Update uses that art on a gamepad that is not a
  PlayStation's, scaled to the port's icon size: a single button's
  icon is the art of the SDL button its input is bound to, so A and B
  swapped on Nintendo-style pads come out right; the sticks, the d-pad
  clusters, the triggers and the right touchpad half use their own.
  runtime_pad_prompt_nintendo() tells the Switch's from the rest.
- Without the files (Pillow missing), or for a button with no art, the
  letters as before; PlayStation pads and the keyboard are unchanged.

The gestures menu without the motion-control icons (the fork's other
part of mod 30) is not brought yet.
2026-11-01 02:39:32 +00:00
ayozetr 39a2174f47 Trophies: the Linux fork's notices, list and chime in place of 0.7's
The fork keeps its own trophy system, which does more than 0.7's:

- scripts/trophies.py (run.sh, at every start) prepares the dump's list
  and icons in <user>/trophies/<NP communication id>: names in the
  game's language with the player's trophy key (BB_TROPHY_KEY), or
  PSN's English names for Bloodborne without one (0.7 shows only the
  trophy's number then), and which trophies count toward the platinum.
- The runtime records each trophy the first time (earned.tsv, with its
  time) and awards the platinum once every trophy that counts toward it
  is in: the base game's, not The Old Hunters'.
- The notice is a card in the top-left corner in the PS4's style with
  the trophy's icon (a cup in its grade's colour without one), and a
  short chime of its own (bbport.ini trophy_sound, a menu checkbox; from
  GerasimosKan's trophies, deadinside28/bloodborne_pc#127).
- BB_TROPHY_PREVIEW=<id> shows a trophy's notice at the first trophy
  call without recording it.

0.7's history file (user/trophies.jsonl) is still written for every
unlock. Its own notice and decryption (bbport_trophies.cpp) are no
longer called. tests/test_trophies.py checks the list, the runtime
tests the platinum without the add-on's trophies. Checked: run.sh
prepares the list and the icons for the game's dump.
2026-11-01 02:36:27 +00:00
ayozetrandEthan Murphy a5e89fd8ac Field of view setting
bbport.ini fov (degrees, 30 to 90; unset or 43 the game's): the game's
camera table (LockCamParam in gameparam.parambnd.dcx) with every
camera's field of view scaled by fov / 43, so the areas that set their
own keep their difference. It is a layer the port makes for the launch,
laid under the player's mods (a mod's own camera table wins): mods.py
takes such layers again (--ui-mods), kept with mods switched off and
left out of the save copies made when the set of mods changes. The
game reads the table at start, so a change needs a restart.

scripts/fromsoft_files.py holds the DCX and BND4 readers and writers it
uses (the Linux fork had them in its quit_menu.py). tests/test_fov.py
checks the table. Checked in game at 60 degrees: 72 cameras, the view
visibly wider.

From Supermedo's port (b75e811, Ethan Murphy), by way of the Linux fork;
the launcher's row comes with the launcher.

Co-Authored-By: Ethan Murphy <[email protected]>
2026-11-01 02:24:17 +00:00
ayozetr e64655f7f6 Overlay: Quit game works from a controller, and the menu stays on screen
- The Quit button's label was its ImGui ID: at the first press the label
  changed, the new button did not have the pad's or keyboard's focus and
  the button disarmed at once, so quitting worked only with the mouse
  over it. "###bb_quit" keeps one ID for both labels.
- The menu sized itself to its contents: on a short screen its last row
  (Close, Quit game) was off screen. Its height is now limited to the
  screen below where it opens, and the contents scroll.

Reported by parsifal295; from the Linux fork.
2026-11-01 02:18:34 +00:00
ayozetr 570ee9b579 On-screen keyboard for the character name, typed with a controller
The game's text dialog (ImeDialog: the character's name) can only be
typed on a physical keyboard in 0.7, so in the Steam Deck's Game Mode or
with a controller on the sofa a character cannot be named. The dialog
now shows a keyboard: the d-pad or the left stick moves, Cross types,
Square deletes, Triangle is a space, L1/R1 switch caps, Options confirms
and Circle cancels. A physical keyboard types as before. A row with the
menu language's own letters (ñ and the accents of Spanish, Italian,
French and Portuguese, German's umlauts and ß) sits above the actions.

The keyboard is drawn in the dialog and its edits are queued to the
window thread, which owns the text and the window (TakeTextEdits). It
and the menu accept and go back on the game's cross and circle wherever
bbport.ini puts them (A and B swapped on Nintendo-style pads:
runtime_pad_button).

From SrShadowy's port (gpu/shim/bbport_keyboard.cpp), reworked in the
Linux fork; its texts are in the menu's languages.
2026-11-01 02:17:34 +00:00
ayozetr fc17a2d7f4 Cheats and gameplay tweaks in the in-game menu
Community patches for 1.09 already in patches/Bloodborne.xml, switched
from bbport.ini as the game effects are: never die, enemies do not see
or hear you, Rally that never fades, enemy control, no Rally, the
camera further away, no camera auto-rotation, running with less stick
tilt and Dark Souls ragdoll physics (cheat_* and tweak_* keys). They are
rows of the menu's effects list, translated in its languages, and apply
after a restart as the other startup patches do. Ported from the
Windows fork (Supermedo's port) by way of the Linux fork; its launcher
page comes with the launcher.
2026-11-01 02:14:46 +00:00
ayozetr afadddfaf9 In-game menu: 0.7's new texts in the fork's languages
The 75 menu texts 0.7 added (anti-aliasing, VBAO, texture quality, low
latency, frame generation, aspect ratio, camera speed, VRAM and frame
rate lines, the upscaler DLL choices, ...) in Spanish, Italian, French,
German, Brazilian Portuguese and Simplified Chinese, with the terms the
menu's other translations use. Every MenuText of the tree now has its
row; printf specifiers and ### ids are kept as in the English.
2026-11-01 02:13:02 +00:00
ayozetrandEthan Murphy 63be02c67f Mute in the background
bbport.ini mute_unfocused (off by default): the audio ports are silent
while the game window does not have focus. They keep their pace, so the
game's audio threads run on as before. A checkbox in the in-game menu
(translated in its languages); the launcher's switch comes with the
launcher.

Adapted from Supermedo's port (b75e811), by way of the Linux fork.

Co-Authored-By: Ethan Murphy <[email protected]>
2026-11-01 02:11:47 +00:00
ayozetr 3ed9330a6a In-game menu in Spanish, Italian, French, German, Brazilian Portuguese and Simplified Chinese
0.7's menu speaks English and Russian (BbSettings::MenuText(english,
russian)). The Linux fork's languages come back on top of it:

- MenuLanguage gets Spanish, Italian, French, German, Brazilian
  Portuguese and Simplified Chinese, with their bbport.ini codes
  (menu_language=es, it, fr, de, pt_BR, zh_CN) and own names in the
  Language selector. Before one is chosen, BB_MENU_LANGUAGE or the
  desktop's locale picks it (MenuLanguageFromCode).
- MenuText looks the English up in bbport_menu_i18n.h for those
  languages (MenuTranslation; the English when a text is missing), and
  MenuTextIn gives a text in any language.
- The pages the port adds to the game's own System menu follow the
  game's text language: bbport.ini game_text_language when it names one
  of these, else the language the game was given (BB_LANGUAGE or the
  locale); Cyrillic in the game's own text still means Russian. The
  game's font has no Chinese characters, so those pages stay in English
  there. The resolution row's "Auto (21:9)" is built in each language.
- Simplified Chinese: the menu's font has no Chinese characters, so the
  first time the menu opens or speaks Chinese a system font is merged
  into it: BB_CJK_FONT (listed in bb_env_vars.def), else fontconfig's
  choice for zh-cn; the log says when there is none.

The translations are the fork's (its main branch's table); 0.7's new
menu texts follow in the next commit.
2026-11-01 02:09:57 +00:00
ayozetr f47ef822ee Menu: sliders keep their changes with any compiler
Slider() passed ImGui::SliderFloat(..., &v, ...) and v to Store() in
one call: the order the arguments are evaluated in is the compiler's,
so Store could get v before the slider changed it, and a drag was lost.
The slider runs first now, as 0.7 already does for checkboxes. From the
Linux fork (542e9ada).
2026-11-01 02:05:35 +00:00
andrecabralgit 2824ea7ab2 Audio: the game at full level on stereo devices
The game opens 8-channel ports and SDL turned them into stereo with its 7.1
matrix, which scales the front pair by about 0.21 so a full 8-channel sum
stays under 1. The game's mix lives in the fronts, so on a stereo device it
played about 13 dB quieter than everything else, on PipeWire too. Ports now
open at the device's channel count when it has fewer, and the runtime folds
the 8 channels down itself with the fronts at unity (stereo, 2.1, quad, 5.1,
6.1). BB_AUDIO_GAIN (0-8) scales the whole output. BB_AUDIO_DUMP names the
channels it writes, the device's after a fold.

From Supermedo/bloodborne_pc#18 (d41e3da) and Supermedo's ff0e421.
2026-11-01 02:05:35 +00:00
ayozetr a48a131590 Keyboard: the Linux fork's classic layout as a choice
0.7's layout (Dark Souls III's on PC: Space the dodge, Left Shift the
run, F and Enter Cross, Escape the menu and the way back in it, IJKL the
camera) stays the default. keyboard_layout=classic in bbport.ini (the
fork's launcher, Controls) gives the keys the fork had up to
0.51-linux.20: WASD and the arrow keys the sticks, Space Cross, Left
Shift Circle, E Square, Q Triangle, 1 L1, 3 R1, R L2, F R2, Z L3, C R3,
Enter Options (also in a game menu, not the way back), Tab and Backspace
the touchpad halves, IJKL the d-pad, Left Alt walk, and the mouse
buttons as before. key.<input> lines still replace either layout's
keys. tests/test_pad.c checks both.

Also bb_env.h's note: env_vars.py prints the list, it no longer writes
docs/ENVIRONMENT.md.
2026-11-01 01:51:35 +00:00
ayozetrandAwaken 8cff10a374 Controller: the Linux fork's stick and button fixes
On 0.7's src/runtime_pad parts, each as on the fork's main branch:

- A stick at the end of its travel is never its neutral (its issue
  #20): an 8BitDo Pro 2 reported -32768 on all four axes right after
  reconnecting, which was taken as a biased neutral, and the camera and
  the player spun.
- A round dead zone: held to the stick's length and mapped from 0 at
  its edge, so a push close to one axis keeps its small part of the
  other one (BB_PAD_DEADZONE_RADIAL=0: per axis). The idea is from
  Lantern Engine.
- A round gate: a stick past it (a diagonal near the square's corner)
  is brought back onto it (pad_round_gate=0: as it comes).
- pad_camera_diagonal=1 evens out the camera stick's diagonals (its
  issue #7: the game's fast turn applies to them too); off by default.
- pad_swap_sticks, pad_invert_look_x and pad_invert_look_y (from
  Awaken's BloodBorneButPc).
- Back + d-pad left/right clicks the touchpad halves on pads without a
  touch surface, Back alone the left half on release (parsifal295's
  idea, as in Supermedo's port 1.7.3).
- Buttons still held when a menu closes stay hidden until released, so
  the game does not take them as a new press (hold_after_capture, from
  Supermedo's port by way of Awaken's).

tests/test_pad.c checks the neutral, the gate, the dead zone and the
stick options. The variables are listed in bb_env_vars.def.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 01:47:47 +00:00
ayozetrandAwaken b902cbc5ff Controller: the one in use takes over, a light bar colour, and pads by device path
From the Linux fork, on 0.7's src/runtime_pad/gamepad.c:

- Several gamepads and none chosen (also one pad behind a 2.4 GHz
  receiver that shows as two): the one being used takes over, checked
  five times a second; used means a button held or an axis well away
  from where it rested when opened, so a wheel's pedals do not count.
  From Awaken's BloodBorneButPc.
- BB_PAD_LIGHTBAR=RRGGBB: every pad with a light bar (DualShock 4,
  DualSense) shows that colour; unset, SDL's own.
- BB_GAMEPAD may also be the pad's device path (/dev/input/eventN or
  hidraw), the only way to tell two pads of the same model apart; the
  log names each pad's path. BB_GAMEPAD_STRICT=1 (local co-op, one game
  per player) takes only the chosen pad, never another instead.

The variables are listed in bb_env_vars.def (next commit).

Co-Authored-By: Awaken <[email protected]>
2026-11-01 01:47:47 +00:00
ayozetrandethan cddb964ebd Crash guards: an online event without its data, from Supermedo's port
The game describes an online event and case 0 copies a string from its
data without checking it: a player on The Hunter's Dream crashed there
(read access to 0xa0 at guest offset 0x1eb56b7) right after the
server's ss.info request timed out. Case 0 with no data now goes the way
of an unknown event, "could not" (scripts/crash_guards.py;
BB_CRASH_GUARDS=0: off, listed in bb_env_vars.def). 0.7 does not touch
the site. patches.py checks the original bytes first (eboot_bytes) and
leaves the guard out, with a line, on other code.

Its code goes in the free tail of the code segment's last page (from
0x50D9700; the game's code ends at 0x50D96DC). The loader refused patch
writes past a segment's size ("bad patch entry"); it now takes them up
to the executable segment's 4 KiB page boundary, which is mapped with it
on every host and unused by the game (patchable(), from the Linux fork).

Checked: the guard's bytes read back from the running game's memory, and
the game plays. tests/test_crash_guards.py checks the code.

Co-Authored-By: ethan <[email protected]>
2026-11-01 01:21:32 +00:00
ayozetr cfc63a6766 Patches: start online at the title when online play is on
Skip Online/Offline Choice answers the title's dialog with PLAY
OFFLINE, so the game's own online session only started after Log In,
and the Kin and Bloodlust Bells stayed greyed out until then (the Linux
fork's issue #9). With online play on (BB_ONLINE=1), a variant now
answers PLAY ONLINE: the same dialog builder returns what the PLAY
ONLINE item returns (operator() at 0x1b49eb0), which differs from PLAY
OFFLINE's only in the online flag. Offline, 0.7's patch stays.
tests/test_patches.py checks both against the builder's items. From
the Linux fork.
2026-11-01 01:01:36 +00:00
ayozetrandAwaken 64f7962cac Patches: "1.09" is "01.09", and copies of Bloodborne.xml are not third-party
- A third-party patch file written for AppVer="1.09" is the game's
  01.09: it was skipped as written for another version.
- A copy of the built-in Bloodborne.xml in the patches folder (an older
  package's, any case) is not taken as a third-party file: it applied
  its frame-rate patches over the port's own.

tests/test_patches.py checks both. From Awaken's BloodBorneButPc, by
way of the Linux fork.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 01:00:55 +00:00
ayozetr 1cb0fd24ed Loading and smoothness: three game patches from bbhost
From droogie/bbhost (patches/*.toml), each checked against the 1.09
executable's original bytes, noted beside it in Bloodborne.xml:

- Quick Re-entry (on): a load after a death or lamp travel ends when the
  area is ready, 2-3 s, not after the 12-second minimum.
- Fast Run-time Deflate (on): the player's recording, compressed every
  ~50 s on the main thread at deflate level 9 (~23 ms in one frame), at
  level 1.
- All Resource Post-Processors In Play (off, experimental): the third
  of the resource system's post-processor threads keeps running.

bbport.ini tweak_quick_reentry, tweak_fast_deflate and
tweak_post_processors (0/1) choose them; the first two are on when the
file does not name them. tests/test_patches.py checks it, and the tests
that count other patches turn them off. From the Linux fork; the
launcher's rows come with the launcher.
2026-11-01 01:00:20 +00:00
ayozetr b2a6130b42 DLC folder: the player's dumped add-ons linked where the runtime looks
0.7 reports <user>/addcont/<title id>/<label> as installed add-ons
(shadPS4's layout). BB_DLC_DIR, the Linux fork's launcher's DLC folder
(one folder per add-on, e.g. SPEXPANSIONDLC03), is now linked there by
run.sh for the game's title id; a real folder already in its place is
left alone. Listed in bb_env_vars.def. The launcher's row comes with the
launcher.
2026-11-01 00:52:55 +00:00
ayozetr ed9d812b85 Runtime: game language follows the desktop without the launcher too
The launcher passes BB_LANGUAGE, but run.sh started directly (scripts,
the terminal) left the game in English. Without BB_LANGUAGE the runtime
now maps the desktop's locale the way the Linux fork's launcher does:
Japanese, English, French, Spanish, German, Italian or Russian, English
otherwise. From the Linux fork.
2026-11-01 00:52:55 +00:00
ayozetrandAndiMajore bdd7994cca Debugging: BB_FAULT_WAIT=1 keeps a crashed process alive for gdb
A fatal fault in bb-probe or a GPU library assertion ended the process
at once, leaving a few lines in the log. With BB_FAULT_WAIT=1 the fault
handler and assert_fail_impl print how to attach, send a desktop
notification and wait instead of exiting; any process of the user may
ptrace it (PR_SET_PTRACER_ANY, set at start only then). Nothing changes
while the game runs. Linux only (fork, notify-send, ptrace).

tools/debug_attach.sh [pid] [--keep] attaches gdb in batch mode and
writes logs/crash-<time>-<pid>.txt: every thread's stack, the faulting
thread's registers and code (game code as a guest offset), and bt full
of an asserting thread (docs/DEBUGGING.md). The variables are listed in
bb_env_vars.def.

From deadinside28/bloodborne_pc#165, by way of the Linux fork.

Co-Authored-By: AndiMajore <[email protected]>
2026-11-01 00:51:22 +00:00
ayozetrandAwaken 4247abab8e Guest memory: the game's in/out arguments are written outside the region lock
Mapping, querying and batch calls read and wrote the guest's pointers
with the region table lock held. A write landing on a GPU-tracked page
faulted into page tracking, which waits for its own locks or a readback
by the GPU thread, while those threads waited for the region lock: a
hang. The arguments now go through locals, copied in before the lock and
out after: direct allocation, direct and flexible maps, virtual range
reservations, protection and virtual queries, direct memory type and
direct queries, and the batch map's entries. Still so in 0.7
(src/runtime_memory/kernel_api.c).

From Awaken's LanternEngine (7aa3d1b), by way of the Linux fork.
Checked: build.sh --test, and four minutes walking in game.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 00:48:48 +00:00
ayozetr fa0d7232d9 Runtime: reservations of guest memory that fail are logged
Failed direct and flexible mappings already printed a line; a failed
direct allocation, virtual range reservation or low memory map returned
its error silently. A guest NULL read soon after one of them would then
look like a bug of its own. Each now prints what failed, its size, the
error and the direct memory in use; the first 16 only. The return values
are unchanged.

The idea comes from Mrsuss60's Windows port (4035ab4e); from the Linux
fork.
2026-11-01 00:39:07 +00:00
ayozetrandAwaken b2f516f0e0 Runtime: failed allocations in audio decoding and the save list are not fatal
- AJM: a job's joined input, sized by the game, is refused above 64 MiB
  or when malloc fails (the job reports an invalid parameter) instead of
  writing through NULL.
- Save list: a failed realloc stops the listing with what it has instead
  of losing the list and writing through NULL.

From Awaken's engine (bbhost-changes.patch, 0.9.24), by way of the Linux
fork.

Co-Authored-By: Awaken <[email protected]>
2026-11-01 00:39:07 +00:00
ayozetrand0xCydral 773368a150 Runtime: split flexible mappings keep their own pool offset
split_at moved the offset only for direct memory, so the right piece of
a split flexible mapping kept the left piece's. Unmapping it then freed
and zeroed the left piece's pages (still mapped) and leaked its own;
read/write backing used the wrong pages too. Still so in 0.7
(src/runtime_memory/vma.c).

From bmy/bbport-mac 9ffc78d, by way of 0xCydral's fork and the Linux
fork.

Co-Authored-By: 0xCydral <[email protected]>
2026-11-01 00:39:07 +00:00
ayozetr 0d091c9335 Runtime: the game's exit() ends the process like its _exit()
The game's own quit uses _exit(), but a title menu patch that adds Exit
Game calls exit(). That ran the game's static destructors while its
other threads kept running: they tore down Dantelion's mutexes under
them ("DLLightMutex.cpp(126) Mutex is not initialized"), and the game
faulted and ended with code 139 instead of 0. The host's atexit handlers
would also free the GPU's objects under the render thread. exit() now
reports, flushes the log and ends the process at once, as _exit() does;
saves are already on disk, written whole. From the Linux fork.
2026-11-01 00:39:07 +00:00
ayozetr 95c548e6f1 Runtime: the game's threads get the PS4's floating-point state
The PS4 runs its threads with flush-to-zero and denormals-are-zero set
in MXCSR (0x9fc0); a Linux thread starts with both clear (0x1f80), and
the runtime never set them. The game's own code then got different
results than on a PS4: very small values stayed as denormals instead of
becoming 0, which a physics step can drift on (the ghosts' cloth in
Cainhurst was reported glitching), and denormals are slow on x86.

The main thread gets it before the first guest code, and every thread
the game creates when it starts. The values are the ones dumped from
real hardware, as in shadPS4#3568 (StevenMiller123, found by red-prig).
From the Linux fork; tests/test_runtime.c checks it.
2026-11-01 00:39:07 +00:00
ayozetr 55eb72e933 Windows: upstream's launchers, packaging and tools left out of the Linux fork
The fork is Linux only and nobody here builds or tests the Windows
package, so its own files go: the WPF launcher (launcher/windows), the
Tkinter one (launcher/bbport_launcher_win.py), the Windows runner
(run.py), the zip's packaging (packaging/windows, packaging/bbport.ico),
FSR 4.1.x's Windows asset build (tools/fsr4cap/build_assets_win.py) and
the Windows benchmarks (tools/win-bench).

The engine's Windows code stays as upstream has it (gpu/platform/win32,
the _WIN32 parts and their CMake rules): leaving it alone keeps
upstream's fixes easy to bring in, and the code portable. Comments that
name the removed files say where that code came from.
2026-11-01 00:22:13 +00:00
ayozetr 40031dd622 Players' guide: its theme, hook and publishing script, published from main only
The fork's players' guide (GitHub Pages, https://ayozetr.github.io/bloodborne_linux/)
is built from docs/ by MkDocs Material: site/ holds the theme's
overrides, the hook that points links to files outside docs/ at them
on GitHub, and the pinned requirements; tools/publish_guide.sh builds it
(--strict) and pushes it to the gh-pages branch. _site/ is ignored.

The site is main's guide, so the script now publishes from main only: a
branch's pages, here 0.7's with links to files main does not have yet,
would replace it. BB_PUBLISH_BRANCH=1 publishes from another branch on
purpose (listed in bb_env_vars.def).

Checked: mkdocs build --strict passes on this branch.
2026-11-01 00:09:31 +00:00
ayozetr 1eadf67c8c Docs: the Linux fork's own, in place of upstream's
The repository's documentation is the fork's: README.md, BBPORT.md (the
original project's README, for its technical details), mkdocs.yml and
docs/ with the players' guide, the developers' pages and the design
notes from upstream the fork keeps in English. Upstream's own docs
(release notes, roadmap, session logs, its other notes, most of them in
Russian) and README.ru.md are not part of it; docs/README.md links to
them in upstream's repository, as on the fork's main branch. Pages that
describe 0.51's code are brought up to date as each part is brought onto
0.7.

docs/ENVIRONMENT.md stays the fork's guide, written by hand.
scripts/env_vars.py no longer writes over it: --doc prints the whole
list of gpu/shim/bb_env_vars.def as Markdown, and tests/test_env_vars.py
checks that list against the code, and that the printed list has every
variable.
2026-11-01 00:05:25 +00:00
ayozetr b8bb54a791 AppImage: its variables in the registry, without the parts not on 0.7
tests/test_env_vars.py checks every BB_* name the scripts use against
gpu/shim/bb_env_vars.def, and the game names any BB_* it does not know
as a typo:

- The host's Vulkan variables the AppImage puts aside under Steam's
  pressure-vessel are named one by one (BB_HOST_VK_DRIVER_FILES, ...)
  and listed as internal, so the log shows them without a warning.
- bbbridge left out: on 0.7 The Hunter's Dream is the online module's
  own pool. The PKG extractor comes back with the launcher's Install
  from PKG.
2026-11-01 00:01:51 +00:00
ayozetr f760e22ebf GitHub: the fork's issue templates and funding link
The bug report (version and build, how the game was installed, the GPU,
settings and the log), the feature request and the template chooser's
links, and the Ko-fi link, as on the fork's main branch. They name the
launcher's About and Log pages, which come with the launcher.
2026-10-31 23:59:43 +00:00
ayozetr c61dc69ae7 License: the Linux fork as a whole under GPL-3.0-or-later
As on the fork's main branch: the whole is distributed under
GPL-3.0-or-later, which everything in it allows (bbport and shadPS4 are
GPL-2.0-or-later, the online module's BBSC and The Hunter's Dream parts
GPL-3.0-or-later, the rest MIT, BSD or Zlib). LICENSE has the GPL-3.0
text. NOTICE keeps bbport's own notice whole (its files stay
GPL-2.0-or-later and its attribution request stands) and says so first.
2026-10-31 23:59:30 +00:00
ayozetr 05807030a2 Git: game files and local runs ignored; the patched FSR-Vulkan is not a change
- .gitignore: the mods folder and mods.last of runs from source, game
  files and dumps (eboot.bin, param.sfo, PKG, DCX, 7z, CUSA folders),
  and an exception for the launcher's icon past the *.png rule.
- .gitmodules: FSR-Vulkan's working tree holds build.sh's patches, so it
  is ignore = dirty: a build's commit (the log's first line, the
  AppImage's BUILD) no longer says "-modified" for them.
2026-10-31 23:59:02 +00:00
ayozetr 64660056b4 Logs: the build next to the version on the first line
Issue reports rarely say which build they ran, and a test build prints
the same version as the release it was made from. run.sh now prints the
build after VERSION: the commit, branch and date from the BUILD file
packaging/appimage-sharun.sh writes, or git's commit and branch in a
checkout ("bbport 0.7, build 0a1b2c3d main 2026-11-01"). From the Linux
fork.
2026-10-31 23:58:46 +00:00
ayozetr c2a45f36e6 Numbers with a decimal point in every language (AppImage)
The AppImage without Nix preloads sharun's anylinux.so, which checks the
host locale with setlocale(LC_ALL, "") and leaves it set. In Spanish,
German, Portuguese and other languages with a decimal comma the game's
process then wrote "72,9 FPS" in the log and "sharpness=0,30" in
bbport.ini, which strtof and atof read back as 0. bb-probe sets
LC_NUMERIC to C at the start of main, after the preload's check; the
rest of the user's locale stays. Checked with the AppImage under
es_ES.UTF-8: "479.7 FPS".
2026-10-31 23:58:27 +00:00
ayozetr 47b276b389 AppImage without Nix (packaging/appimage-sharun.sh)
The Linux fork's AppImage, brought onto 0.7: the port's build, its
scripts and the GTK4 launcher with every library they load (glibc,
Python with PyGObject, ssl and hashlib, GTK 4, libadwaita, SDL3, Mesa's
Vulkan drivers for AMD and Intel) collected from the build system by
quick-sharun at a fixed revision. NVIDIA's driver comes from the host.

On 0.7's layout: run.sh, VERSION, LICENSE, NOTICE and the shader
recipes when the build has them; the online module is
out/gpu/libbbnet20.so, handed over in BB_NET_MODULE (BBSC is inside it
now). The log's first line gets the build (BUILD).

- DLSS: with sharun /proc/self/exe is the dynamic loader, so the bridge
  and NVIDIA's libraries are looked for in BB_DLSS_DIR first
  (gpu/upscale/vk_dlss.cpp, listed in bb_env_vars.def).
- packaging/bbport-entry.c keeps 0.7's Steam overlay handling and the
  fork's pressure-vessel one (Steam's Game Mode, deadinside28#172).
- The launcher's icon (launcher/bloodborne.png, added past the
  .gitignore's *.png rule) in the desktop entry, also for
  launcher/install-desktop.sh.

Tested: the AppImage starts the game with --play in its own data and
config folders and plays at about 70 FPS on an RTX 3060.
2026-10-31 23:54:49 +00:00
ayozetr 71e6cc9592 FSR-Vulkan: the shader override and job mark hooks the port uses
0.7 sets ffxVkShaderOverride (gpu/upscale/ffx_shaders.cpp: the frame
generation passes built in this tree) and ffxVkJobMark
(gpu/upscale/fg_profile.cpp, BB_FG_PROFILE), but no patch to the FFX
Vulkan backend defined them, so a build from source failed to link
bb-probe (upstream #404). Patch 0003 defines both as null pointers and
calls them: the override where the pass's shader module is made (the
bindings stay the packaged blob's), the mark before each compute
dispatch.
2026-10-31 23:24:14 +00:00
+24 0fc2921c45 0.7: native Windows build with online and mods, one memory model, faster heavy views, cloth and shadow fixes
- Windows 10/11: native bb-probe.exe (llvm-mingw / MSYS2 CLANG64), WPF launcher in five
  languages; online module online\bbnet20.dll with both pools (Seamless Hunt + BBSC, The
  Hunter's Dream with Discord sign-in) and BBSC's local game server; mods and XML patches from
  the launcher (junctions and hard links without Developer Mode); DLSS / DLSS FG, FSR 4.1.1 and
  FSR 4.1.2 driver models, FSR 4 from AMD's SDK 2.4 DLLs; NVAPI power profile on NVIDIA.
- The old memory model of 0.3 removed; the game's graphics heaps grow on demand.
- Faster: occlusion queries counted only inside the game's open queries (164 -> 189 FPS in the
  heaviest view), ring dedup and copy-shader filter (less PCIe traffic), PGO retrained, Steam
  Deck draw preparation, FSR 3.1 FG shaders.
- Fixes: cloth fluttering above 30 FPS (#58), point-light shadow wedges, HUD item-icon grid,
  vertex explosions across heap pieces, FPS drop after a save switch, FG ghosts, crash
  0x2169edc (#336), FSR 3.1 protected memory (#85), Wayland HiDPI, launcher translations (#381).
- New: AA menu (FXAA2_GS, SMAA), FSR 1, GT-VBAO, AMD Anti-Lag 2, trophies.jsonl, shader
  recipes before play, live resolution by default.
- Packages: Windows zip, AppImage, and a Linux folder (tar.zst) with the online module open.
- Removed: the shadp2p online module, LEGO Dimensions toy pad and NGS2 code.
- Credits: docs/CONTRIBUTORS_0.7.md (pull requests since 0.4 and outside work).

Co-authored-by: Qiming-Liu <[email protected]>
Co-authored-by: AndiMajore <[email protected]>
Co-authored-by: adds39939 <[email protected]>
Co-authored-by: iannitram <[email protected]>
Co-authored-by: maiconjsv <[email protected]>
Co-authored-by: whp199 <[email protected]>
Co-authored-by: DominickCobb-rs <[email protected]>
Co-authored-by: Sarxzer <[email protected]>
Co-authored-by: wheat32 <[email protected]>
Co-authored-by: ArenaMMORPG <[email protected]>
Co-authored-by: SrShadowy <[email protected]>
Co-authored-by: Gustavo Bomfim <[email protected]>
Co-authored-by: Teefpyre <[email protected]>
Co-authored-by: SrDeTs <[email protected]>
Co-authored-by: IgorWarzocha <[email protected]>
Co-authored-by: itachi-re <[email protected]>
Co-authored-by: Bainianzzz <[email protected]>
Co-authored-by: hideoneric <[email protected]>
Co-authored-by: guitaripod <[email protected]>
Co-authored-by: unTanana <[email protected]>
Co-authored-by: andrR89 <[email protected]>
Co-authored-by: ayozetr <[email protected]>
Co-authored-by: JuanraCM <[email protected]>
Co-authored-by: SaeedShow <[email protected]>
Co-authored-by: msisovic <[email protected]>
Co-authored-by: axpico <[email protected]>
Co-authored-by: Aryxa <[email protected]>
Co-authored-by: Obrikash <[email protected]>
Co-authored-by: noelmiller <[email protected]>
Co-authored-by: AyrtonSanabio <[email protected]>
Co-authored-by: Supermedo <[email protected]>
Co-authored-by: Awaken <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:55:53 +03:00
deadinsideandClaude Opus 5.5 8185b88d80 0.6: online on shadPS4 0.20 (Seamless Hunt + BBSC, The Hunter's Dream), Laurence cinematic, flicker and load crash fixes
- Online module libbbnet20.so: shadPS4 v0.20 network/NP/shadNet code, BBSC ported to it,
  The Hunter's Dream backend (droogie/bbhost) with Discord sign-in; launcher pool choice;
  the shadp2p module stays as a fallback (BB_NET_GEN=19).
- Laurence's cinematic above 30 FPS: render-job slots given back; the render resource cache
  gets 512 MiB of port direct memory past the game's pool (the game's heap unchanged).
- Single shifted/quarter frames: push constants recorded after the pass begins (the occlusion
  results' compute dispatch overwrote them).
- Crashes: 0xa0020007 (#207, blur-kernel sample clamp), Bad PM4 at save load (FX init clear
  race, command buffers copied at submit), null V# tables (139).
- Light shafts on NVIDIA (#204, PR #210), #205, texture GC on 8 GB, PR #206/#224/#214/#211,
  FSR 4.1.1 reset after save change, SSAO forced back, SSR at start, present wait mask,
  Polaris labels on the CPU (#203), AUR GE-Proton (#186), 21 game languages (#249).
- Live: F3 30 FPS limit, effects without a restart, keyboard stick/run/dodge/jump, cursor,
  GPU choice in the launcher; more draw-preparation workers on 12+ thread CPUs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 09:30:55 +03:00
deadinsideandClaude Opus 5.5 72e5c8ce22 0.6-pre1: new memory model on every GPU, seamless co-op, aspect ratios and frame rate by hooks, own renderer
- Auto memory model = the new one on every GPU (layer memory module, no sparse binding);
  the 0.3 model stays as a launcher option.
- Seamless co-op (BBSC) in the online module; Seamless Hunt servers by default; the shadNet
  client reports its version and stays offline when a server refuses it.
- Any aspect ratio live through game hooks (camera, HUD anchoring, UI fills, markers);
  frame rate through hooks instead of patches.
- Keyboard and mouse: native mouse camera, menu mouse pointer, Dark Souls III-like layout,
  chord bindings, button prompts for the device in use (HUD atlas included).
- Real occlusion queries; own command processor, GCN definitions, shader translator,
  renderer and caches instead of shadPS4 (byte-identical SPIR-V on the game's shaders).
- Fixes: NVIDIA start (#135 #147 #152), RIP=0 (#161 #177 #181), #149, #142, #162/#192,
  #178, #144/#165/#166, #134, #172, #186, #136, #145, lamp-travel loading hang (PR #193),
  FPS aging, write-trap race, exit crash, pipeline-cache stages, AppImage --play.
- Runtime tests follow the NID-only import rule.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 12:18:28 +03:00
deadinsideandClaude Opus 5.5 3c57da4198 0.51: online play (shadNet module), mouse look, DLSS in the AppImage, fixes, 399 MB AppImage
- Online play through shadNet as a separate module (gpu/bbnet -> libbbnet.so), loaded by the
  runtime only with BB_ONLINE=1; launcher Online page; UPnP maps the P2P port (#121).
- Mouse look and mouse/wheel bindings (#51, #52); quit from the overlay (#74, #116).
- DLSS bridge and NVIDIA's runtime library in the package; a newer one chosen in the launcher.
- Every retail release with update 1.09 gets the game profile and passes the game check
  (#119, PR #117).
- New memory model: motion blur smear (#122); 4 GB VRAM out-of-memory stop; title menu
  flicker above 1080p.
- HTTPS certificates in the AppImage; NVIDIA Wayland swapchain (PR #93); git build after 0.4
  (#124); nix-shell launcher arguments (PR #120); FSR 4.1.1 recording on SteamOS (PR #89).
- AppImage 925 -> 399 MB: Vulkan-only Mesa (RADV without LLVM, ANV), stripped game binary,
  slimmer MangoHud, SDL3 and launcher GTK.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 01:57:36 +03:00
deadinside c7e3e55eff 0.5: native game settings, AMD memory model by default, GPU translation and driver fixes
Default to the new memory and translation model on AMD using its sparse arena,
and retain the 0.3 memory model on other GPUs unless selected manually.
Add the command decoder, game profiles, shared memory traps and GPU timestamps;
reduce VRAM churn and background pipeline compilation artifacts.

Finish the game's Display and Effects settings pages, add the launcher's memory
model selector, host NVIDIA EGL discovery and RADV nohiz for affected older AMD
GPUs. Include the Proton and ImGui integration fixes and updated release notes.
Keep MangoHud startup behavior exactly as in 0.4.

Validation: 108 Python tests, runtime tests and command decoder tests passed.
The rebuilt public AppImage's MangoHud startup block matches 0.4 byte for byte.
2026-10-10 16:22:21 +03:00
deadinsideandClaude Opus 5.5 10baf9df25 0.5-pre4: memory module without stutter, background pipelines, damaged-extraction check
Rewrite only the page table entries that changed after a promotion, with one runtime lookup per mapping instead of one per block (whole-mirror rewrites under the runtime's memory lock stalled the GPU thread 30-70 ms while areas streamed in). Keep ranges in place after a second write bounce, set write traps per run of blocks, give VRAM copies a floor of the budget on 4 GB cards, let stale copies yield and compact mirrors after travelling. Compile new pipelines of per-frame passes on worker threads. Report damaged game extractions (#81), add NVIDIA driver diagnostics to --vulkan-info (#107), never leave the current command chunk null (#100), restore the 0.4 settings menu look, and add layer timing to stall diagnostics. Bilingual release notes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 22:22:43 +03:00
deadinsideandClaude Opus 5.5 f2cd4ca0c4 0.5-pre3: memory module on NVIDIA: VRAM copies, one VRAM budget, watched blocks
Re-enable VRAM copies for the memory module on host-import GPUs (the fixed-arena workaround no longer applies to it). Keep copies short of the texture collector's critical mark and let idle copies and their growth room yield near it, give growing copies room instead of reallocating them on every load, write WRITE_DATA into VRAM copies too, and watch CPU-written blocks with write traps so they get VRAM copies re-uploaded after CPU writes. Retry file reads on EFAULT, add BB_VRAM_LIMIT_MB for small-VRAM tests, and include bilingual release notes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 17:36:44 +03:00
deadinside af9e66db2f 0.5-pre2: memory layer, native GPU copies and portable labels
Add the memory layer for non-AMD GPUs and preserve coherent guest/VRAM views. Translate the original copy shader by default in the new model, add standard Vulkan completion labels, and migrate versioned shader caches before loading. Include GPU regressions, launcher support and bilingual release notes.
2026-10-09 14:27:09 +03:00
deadinsideandClaude Opus 5.5 39940f25eb 0.5-pre1: settings in the game's System menu, no online/offline screen, DLC, DLSS, save safety
Game menu
- The port's settings as native pages of the game's System menu (Display, Game effects,
  Game patches after Screen/Sound), built and drawn by the game's menu code: hooks on its
  message lookup and item adder, rows bound to the port's values, the game's "Default" sets
  the port's defaults, texts in the game's language (gpu/shim/bbport_game_menu.cpp,
  BB_GAME_MENU=0 off).
- The online/offline choice is skipped by a game patch ("Skip Online/Offline Choice"): the
  game opens its main menu offline (BB_SKIP_NETWORK_CHOICE=0 shows it).
- Overlay menu restyled like the game's dialogs (tabs, rows with values, own focus, serif font).

New
- DLC folders user/addcont/<title>/<label> reported as installed add-ons (#33, PR #84).
- DLSS on Linux through gpu/dlss_bridge when built with NVIDIA's SDK (PR #83).
- Monitor choice (#69, BB_DISPLAY); launcher log copy/export (PR #78), Simplified Chinese
  (PR #75).

Fixes
- Save files written to a temporary copy, fsync and rename: no damaged saves on a crash (#64).
- fsr4cap runs Proton without Steam's container when it cannot start (#61).
- System MangoHud no longer disabled with the bundled one (#66).
- UI draws without a color target go into the UI depth: loading screen item picture (#67).
- Debug menu fonts from adhoc/font, case-insensitive file fallback; no crash without fonts.
- VRAM exhaustion for guest memory blocks falls back to system memory; readbacks=Precise
  no longer deadlocks the first cutscene; indirect dispatch guard (#34); VRAM budget for
  promotion; UTF-8 everywhere in scripts (#55); plain ELF eboot (#49); Switch Pro clone stick
  calibration (#70, PR #71); BB_FSR4_STATS without pipeline statistics (PR #80); GPU info in
  the log (#56).

New memory and translation model (experimental, off by default)
- Offered on NVIDIA too (host memory import, untested there); GPU-written EOP/EOS labels;
  occlusion queries as Vulkan queries; COPY_DATA, COND_EXEC, predication translated; the
  game's copy shader runs as itself; native detours instead of int3 traps in the game's code.

Details: docs/CHANGES_after_0.4.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 00:34:14 +03:00
1427 changed files with 266319 additions and 145924 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# GitHub's language bar: this C++ table of game texts (its <?name?> placeholders) is not PHP.
gpu/bbnet20/bbsc/core/bbsc_messages.inc linguist-language=C++
gpu/bbnet20/bbsc/core/bbsc_translations.inc linguist-language=C++
+1
View File
@@ -0,0 +1 @@
ko_fi: ayozetr
+84
View File
@@ -0,0 +1,84 @@
name: Bug report
description: The game or the launcher does something wrong, crashes or does not start.
labels: [bug]
body:
- type: markdown
attributes:
value: |
Thanks for reporting. Before you start, [docs/TROUBLESHOOTING.md](https://github.com/ayozetr/bloodborne_linux/blob/main/docs/TROUBLESHOOTING.md) covers the common problems.
Only your own dump of the game with the 1.09 update is supported. Do not ask for or link game files.
- type: input
id: version
attributes:
label: Version
description: The launcher's **About** page shows it, with the build of a test build; so does the first line of the log.
placeholder: 0.51-linux.2
validations:
required: true
- type: dropdown
id: install
attributes:
label: How you run it
options:
- AppImage
- From source (run.sh / the launcher in a checkout)
- Steam Deck (AppImage)
validations:
required: true
- type: input
id: system
attributes:
label: Distribution, GPU and driver
placeholder: CachyOS, RTX 3060, NVIDIA 580.95 / Fedora 43, RX 7800 XT, Mesa 25.2
validations:
required: true
- type: input
id: graphics
attributes:
label: Upscaler, preset and output resolution
description: From the launcher's **Settings** page or the in-game menu (Insert / L3+R3).
placeholder: FSR 3.1, Quality, 2560x1440
- type: textarea
id: what
attributes:
label: What happened
description: What you saw, and what you expected instead. Screenshots or a short video help.
validations:
required: true
- type: textarea
id: steps
attributes:
label: How to make it happen
description: Where in the game, and what you did right before. Say if it happens every time.
placeholder: |
1. Load the save at the Cathedral Ward lamp
2. Walk towards Old Yharnam
3. The game closes during the load
- type: dropdown
id: online
attributes:
label: Online
options:
- Offline
- Online, shadPS4's public server
- Online, my own server
- type: textarea
id: log
attributes:
label: Log
description: |
In the launcher's **Log** page, **Export log…** and drag the file here (or **Copy** and paste it).
Do not paste your shadNet password or your trophy key anywhere in the issue.
placeholder: Drag the exported log here.
validations:
required: true
- type: checkboxes
id: checks
attributes:
label: Checks
options:
- label: I use my own dump of Bloodborne with the 1.09 update.
required: true
- label: I searched the existing issues for the same problem.
required: true
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Troubleshooting
url: https://github.com/ayozetr/bloodborne_linux/blob/main/docs/TROUBLESHOOTING.md
about: The common problems and what to do, before opening an issue.
- name: Online play and your own server
url: https://github.com/ayozetr/bloodborne_linux/blob/main/docs/ONLINE.md
about: Accounts, the connection test, summons and the private server guide.
@@ -0,0 +1,21 @@
name: Feature request
description: Something the port or the launcher could do.
labels: [enhancement]
body:
- type: textarea
id: what
attributes:
label: What you would like
description: What it should do, and when you would use it.
validations:
required: true
- type: textarea
id: why
attributes:
label: Why
description: What it solves for you, or what you do today instead.
- type: textarea
id: elsewhere
attributes:
label: Seen elsewhere
description: Another port, fork or mod that already has it (a link helps a lot).
+8 -2
View File
@@ -23,8 +23,6 @@ perf.data
/bloodborne_upscalers_*.md
mods.json
patches.json
# The mods folder of runs from source (the launcher's default, DATA_DIR/mods)
/mods/
# Local: game logs, messages and archives from users, internal plans
/logs/
/message*.txt
@@ -32,6 +30,10 @@ patches.json
/docs/ISSUE_ACTION_PLAN_*.md
# AMD FidelityFX DLLs (FSR 4.1.1 is built from the user's own): never in the repository
*.dll
# The mods folder of runs from source (the launcher's default, DATA_DIR/mods), and the list of the
# last run's mods
/mods/
/mods.last
# Game files and dumps: never in the repository
eboot.bin
param.sfo
@@ -39,3 +41,7 @@ param.sfo
*.dcx
*.7z
/CUSA*/
# The launcher's icon (shipped)
!launcher/bloodborne.png
# The players' guide, built by mkdocs (mkdocs.yml)
_site/
+294 -115
View File
@@ -1,62 +1,80 @@
THIS PROJECT IS NOT RELATED TO SHADPS4. ALL QUESTIONS RELATED TO THIS PROJECT SHOULD BE SENT TO THE DISCORD SERVER https://discord.gg/KYZRKk9CB, NOT TO THE SHADPS4 SERVER.
# bbport — a native Linux port of Bloodborne
# bbport — a native Linux and Windows port of Bloodborne
> This is the README of the original project (deadinside28/bloodborne_pc), kept for its
> technical details. The fork's own README is [README.md](README.md).
**English** · [Русский](BBPORT.ru.md)
> This is the README of the original project (deadinside28/bloodborne_pc, version 0.7), kept for
> its technical details. The Linux fork's own README is [README.md](README.md).
bbport is the counterpart of Wine + DXVK for a single game: *Bloodborne* for PlayStation 4
(CUSA03173, game version 1.09) on an x86-64 Linux PC. The game's original executable runs
directly on the PC:
(CUSA03173, game version 1.09) on an x86-64 PC with Linux or Windows 10/11 (since 0.7, see
[Windows](#windows)). The game's original executable runs directly on the PC:
- **as in Wine**, the game's x86-64 code runs on the CPU directly, and a runtime written for
this one game replaces the PS4 system libraries;
- **as in DXVK**, the game's graphics are translated to Vulkan — by a renderer derived from
[shadPS4](https://github.com/shadps4-emu/shadPS4) and heavily extended for this game, including
temporal upscaling with AMD FSR 3.1, FSR 4 and FSR 4.1.1;
- memory and the GPU's work are moving to the PC model: the launcher has an experimental
*New memory and translation model* mode (AMD GPUs only); without it the game runs on the old
memory model, as in 0.3.
- **as in DXVK**, the game's graphics are translated to Vulkan — by the port's own renderer and
shader translator (they started from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s and were
rewritten), including temporal upscaling with AMD FSR 3.1, FSR 4, FSR 4.1.1 and NVIDIA DLSS,
and DLSS Frame Generation;
- memory and the GPU's work follow the PC model on every GPU: the new memory and translation
model is the only one (the default since 0.6; the old model of 0.3 was removed in 0.7).
Two steps remain to the full Wine + DXVK model: make the new mode the default, and move the
reading of resource descriptors (textures, buffers) from the CPU to the GPU (see below).
> **No game files are included.** You need your own dump of Bloodborne (CUSA03173, v1.09).
> **No game files are included.** You need your own dump of Bloodborne with update 1.09: any retail
> release (CUSA00900 in the US, CUSA03173 for the European Game of the Year edition, ...), as they
> share the same executable.
> This project is not affiliated with Sony Interactive Entertainment, FromSoftware or AMD.
**Status: experimental, playable.** The game boots, loads saves and plays (the Hunter's Dream
and several areas of Yharnam were played with it) with sound, gamepad and saving.
A full play-through has not been verified, and only one machine (Linux, AMD Radeon RX 7800 XT,
Mesa/RADV) has been tested thoroughly.
A full play-through has not been verified. Tested thoroughly: AMD Radeon RX 7800 XT (Linux with
Mesa/RADV, and Windows 11), the Steam Deck; on Windows also two laptops with NVIDIA GTX 1050 and
GeForce 930MX; other NVIDIA and Intel GPUs by players.
## Highlights
- **Native execution.** The eboot is converted offline into a flat memory image; PS4 libc and
libSceFios2 are linked into it as native code. No CPU emulation and no per-instruction
translation: the game code runs at full speed.
- **Two modes** (launcher → *Mode*):
- **Switch off (the default) — as in 0.3.** The old memory model: VRAM copies of the game's
memory, writes tracked through page protection. With every fix made since 0.3 (motion
vectors and the upscaler, flicker with DoF on, a damaged shader cache).
- **New memory and translation model — experimental, AMD GPUs only.** The game's memory lives
- **One memory and translation model** (the model of 0.3 is gone):
- The game's memory lives
in system RAM and the GPU reads it where it is, as a PC game's buffers; data it reads often
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). No
write tracking, no copies of the whole GPU-visible memory. The PS4 command processor's work
is kept in VRAM and given back when unused (textures after 20 s, buffers after 60 s). The
layer's memory module tracks CPU writes to mirrored blocks to keep those copies coherent;
it does not copy the whole GPU-visible memory. The PS4 command processor's work
is translated into Vulkan commands rather than emulated on the CPU: memory writes
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written once
the GPU has really finished the work. Where the game is CPU-bound it runs 20–25% faster
(measured standing in the Hunter's Dream), with fewer stutters. **It may crash**, and has
been tested thoroughly only on the author's PC (RX 7800 XT). On NVIDIA and Intel the switch
is unavailable: NVIDIA's driver cannot map the game's memory as needed, Intel is untested.
Without the launcher: `BB_PC_MODEL=1`; to try it on another GPU: `BB_PC_MODEL_ANY_GPU=1`.
(`WRITE_DATA`, DMA) are done by the GPU in command-stream order, and fences are written by the
GPU itself at the end of the pipeline, at the same point of the stream as the PS4's command
processor writes them. On every GPU the game's memory goes through the layer's memory module:
bound in place, VRAM copies as separate buffers, no sparse binding (NVIDIA's sparse binding
froze the GPU for seconds; on AMD the module is faster than the former sparse arena too);
`BB_LAYER_MEMORY=0` gives AMD's sparse arena. Tested thoroughly on an RX 7800 XT; on NVIDIA
by testers. A driver that fails the startup checks stops the game with a line beginning
`STOP: the GPU driver cannot use the game's memory in place`.
Unused textures are freed in both modes, so VRAM no longer grows with every area visited.
- **Unlocked frame rate.** Community patches (`patches/Bloodborne.xml`) make the simulation
use the real frame time; ~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at
1440p with FSR 4 Quality. Also 30/60/90 FPS modes.
The graphics heaps of the game grow as it takes memory and give it back when freed.
[Changes in 0.7](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.7.md), [0.6](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6.md), [0.6-pre1](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6-pre1.md),
[0.51](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.51.md), [0.5](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.5.md).
Unused textures are freed, so VRAM no longer grows with every area visited; *Texture quality*
(full, half, quarter, auto = half on cards with up to 3 GB) skips the top levels of big
textures on small cards.
- **Unlocked frame rate.** Hooks in the game's code make the simulation use the real frame time
(the community patches in `patches/Bloodborne.xml` remain for comparison, `BB_FRAME_HOOKS=0`);
~90 FPS at 4K with FSR 4 Balanced on an RX 7800 XT, ~150 FPS at 1440p with FSR 4 Quality.
Also 30/60/90 FPS modes; **F3** switches a 30 FPS limit on and off while playing (some
cinematics, e.g. Laurence's, are safest at 30).
- **Any aspect ratio, live.** 4:3, 16:10, 16:9, 21:9 and 32:9 with the HUD at the screen's edges,
through hooks (the game's code is not patched); cinematics are wide too.
- **Keyboard and mouse.** Mouse look turns the game's camera directly; a mouse pointer in the
game's menus (F1 releases the mouse); a layout close to Dark Souls III's (Space dodge, Left
Shift run, Shift+Space jump); movement keys ramp up like a stick; button prompts show the bound
key, or the buttons of the gamepad in use (Xbox and others), PlayStation icons only with a
PlayStation pad.
- **Online play and seamless co-op.** Two pools on shadPS4 0.20's network stack: **Seamless
Hunt** (default; bells, summons, messages, bloodstains and seamless co-op, BBSC, up to five
hunters; an account from seamlesshunt.com) and **The Hunter's Dream** (sign-in with Discord; no
BBSC).
- **Temporal upscaling built for this game.** Bloodborne has no velocity buffer, so bbport
computes motion vectors itself: camera motion from depth and the scene matrices, and object
motion (characters, cloth, weapons) from the vertex positions of the previous frame. The
@@ -71,6 +89,18 @@ Mesa/RADV) has been tested thoroughly.
9000; picked automatically). The assets are built on your machine from one DLL of your own
(4.1.x): the launcher's *FSR 4.1.1 from your own AMD DLL → Choose DLL…* button (2–5 minutes,
twice that on RDNA4; needs a recent Proton), or `tools/fsr4cap`.
- **DLSS Super Resolution and DLSS Frame Generation** (NVIDIA GeForce RTX) through an
optional bridge built against NVIDIA's SDK (see Building). Frame generation makes the frames
between two of the game's (2x on RTX 40, up to 4x on RTX 50) from the finished frame, the
frame before its UI, the scene depth, the motion vectors and the game's camera matrices;
*Overlay menu → Frame generation*, or the launcher.
- **FSR 4.1.2's models from the AMD driver** (INT8 on RDNA2/3 and the *Lite* model), taken
from the driver's `amdxcffx64.dll` with the upscaler DLL (*FSR 4 model* in the menus).
- **FSR 1** (spatial, no motion vectors) for weak GPUs, and **FSR 3.1 frame generation**
(experimental, every GPU; the port's own optical-flow and interpolation shaders).
- **Anti-aliasing** separate from the upscalers: *auto* (the game's AA only without an
upscaler), the game's, FXAA (Kyo's *Force FXAA2*), SMAA 1x (the port's own, before the UI), off;
sharpening (RCAS) also without an upscaler. **GT-VBAO** can replace the game's SSAO.
- Faster than AMD's own shaders on RDNA3: the final passes of FSR 4 and 4.1.1 were rewritten
to store through workgroup memory (3.5× and 2.3× faster, bit-exact); FSR 4 costs ~4 ms at
4K on an RX 7800 XT instead of ~6 ms.
@@ -78,10 +108,19 @@ Mesa/RADV) has been tested thoroughly.
and draws are bound and recorded on another (two-stage pipeline), with a Vulkan recording
thread and helper threads for memory copies. Early on the single GPU thread capped the game
at ~26 FPS; now it runs at 90–150 FPS depending on resolution and scene.
- **In-game menu** (Insert or L3+R3): upscaler, preset, sharpness, output resolution, game
effects (chromatic aberration, DoF, motion blur, SSAO, the game's own AA, SSR, model LOD).
It opens where it was left, with the mouse cursor shown over it.
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
- **Settings inside the game's own menu:** *System → Display / Effects* (after *Screen/Sound*),
built and drawn by the game itself — output resolution, upscaler, preset, sharpness, FPS
counter, model detail and the game's effects, in the game's language. Chromatic aberration,
depth of field, motion blur, SSAO, the game's AA and dynamic-light shadows switch at once;
screen-space reflections, the intro, free camera and the debug menu need a restart.
- **Overlay menu** (Insert, or hold L3+R3), styled like the game's dialogs: everything above plus the
advanced settings, with gamepad, keyboard and mouse.
- **Shaders before you play.** A pack of shader recipes from players' caches (655 programs, 1149
pipelines) is compiled in the background at start, and the cache survives updates: a repeat
session compiles nothing.
- **Launchers:** GTK4 on Linux, WPF in the Windows 11 style on Windows (both with sections and
descriptions); an **AppImage** for Linux and the Steam Deck, the same as a **folder**
(`tar.zst`, `Bloodborne.sh`), a **zip** for Windows.
## How it differs from shadPS4
@@ -89,16 +128,17 @@ Mesa/RADV) has been tested thoroughly.
|---|---|---|
| Scope | General PS4 emulator, many games | One game: Bloodborne v1.09 |
| Loading | Its own ELF loader and kernel emulation at run time | The eboot is converted offline (`scripts/`) into an image with PS4 libc/Fios2 linked in; a C loader maps it and jumps into the game (loader and runtime: ~5k lines) |
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | By default the same model (as in 0.3); in the *New memory and translation model* mode (AMD): the game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | By default the same; in the new mode translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
| Memory | The GPU's view of PS4 memory is kept in VRAM copies, synchronized through page-protection write tracking | The game's memory in system RAM, used by the GPU in place, frequently read data in VRAM, freed when unused |
| Command processor | Emulated: memory writes, DMA and fences are done by the CPU while decoding | Translated into Vulkan commands that the GPU runs in stream order, fences written after the work has really finished |
| System libraries | Broad HLE of the PS4 OS | A small runtime (`src/runtime_*.c`) that implements exactly what Bloodborne calls: memory, threads, sync, files, audio (incl. ATRAC9), pad, saves, AppContent |
| GPU | shadPS4 video core and shader recompiler | The same core (vendored, GPL) with ~200 marked changes (`bbport:`) plus new modules: two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, frame capture and GPU profiler |
| GPU | shadPS4 video core and shader recompiler | Its own command processor, renderer, caches and shader translator (they started from shadPS4's, `gpu/VENDOR.txt`): two-stage draw pipeline, render-state and texture-set memoization, render-scale proxies, motion vectors, FSR 3.1/4/4.1.1, DLSS, frame capture and GPU profiler |
| GPU thread | One thread processes the whole command stream (the bottleneck in Bloodborne) | Decode and draw recording run on separate threads; the work scales with the hardware threads (Steam Deck included) |
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1) with the game's own motion vectors and jitter |
| Upscaling | — | Temporal (FSR 3.1, FSR 4, FSR 4.1.1/4.1.2, DLSS) with motion vectors computed for the game and jitter; frame generation (DLSS, FSR 3.1) |
| Platforms | Linux, Windows, macOS | Linux and Windows (x86-64) |
| Game patches | Patch files applied by the emulator | The same community patches, compiled at start (`scripts/patches.py`); render resolution, effects and FPS from the launcher |
Without shadPS4 there would be no bbport: its renderer and shader recompiler are the base of
the graphics side.
Without shadPS4 there would be no bbport: its renderer and shader recompiler were the starting
point of the graphics side.
### Wine + DXVK for one game
@@ -107,35 +147,40 @@ the graphics side.
- **System libraries.** As Wine replaces the Windows API, the bbport runtime (`src/runtime_*.c`)
implements exactly the PS4 OS functions Bloodborne calls: memory, threads, files, audio, pad,
saves.
- **Memory.** In the new mode, as in a PC game: the game's data in system RAM, VRAM used the way
a PC game uses it. The mode is still experimental and runs on AMD GPUs only.
- **Memory.** As in a PC game (the default model): the game's data in system RAM, VRAM used the
way a PC game uses it.
- **Graphics.** The PS4 GPU's command stream (PM4) is a recording of the game's graphics API
calls: it is written by 99 functions of the statically linked libGnm, so decoding the stream
and translating the calls themselves come to the same thing. GCN shaders are translated to
SPIR-V. In the new mode the command processor's work (memory writes, DMA, fences) is
translated into Vulkan commands as well. One part still works the old way: the translator
reads resource descriptors (textures, buffers) from the game's memory on the CPU, and
recognises textures by address. The next big step is the GPU reading the descriptors itself
(bindless), with textures as objects created at load time.
translated into Vulkan commands as well; fences are written by the GPU itself. Resource
descriptors (textures, buffers) are read by the translator on the CPU, as DXVK reads resource
bindings: a PS4 texture has to be converted from the PS4 tiling into a Vulkan image before the
draw runs, so the CPU must know its descriptor beforehand — translation, not emulation. The
descriptor tables (the constant engine's dumps) are read only by the translator, so the CPU
performs them. The game's occlusion queries (~60 a frame) are
Vulkan occlusion queries; predication, `COPY_DATA` and `COND_EXEC` are translated too (check:
`BB_PM4_SELFTEST=1`). Details:
[docs/EMULATION_REMOVAL_PLAN.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/EMULATION_REMOVAL_PLAN.ru.md), "Шаг 4" (Russian).
"Port" here means a build for this one game, not a rewrite of its source code, which the project
neither has nor includes.
## Requirements
- Linux x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26 (RADV).
The *New memory and translation model* mode needs an AMD GPU.
- Linux x86-64 or Windows 10/11 x86-64, a Vulkan 1.3 GPU. Tested: AMD RX 7800 XT with Mesa 26
(RADV) and with AMD's Windows driver; NVIDIA and Intel by players (please report how they run).
FSR 4 / 4.1.1 require shader Float16, Int8/Int16, integer dot products, linear compute
derivatives and extended storage image formats; FSR 4.1.1 additionally requires
`VK_VALVE_shader_mixed_float_dot_product`. Unsupported choices fall back to FSR 3.1
before the first frame and are disabled in the in-game menu.
- Your decrypted game dump: the `CUSA03173` folder (eboot.bin, sce_module, ...), version 1.09.
The EU release (`CUSA00900`) runs too: its 1.09 executable is the same as the US one.
A dumped update is a separate folder: copy it over the base game, replacing files. The base
game alone (1.00) crashes at start (guest offset 0x20348b8); the launcher and `run.sh` check
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). Some dumps
ship the 1.09 eboot.bin with Lance McDonald's 60 fps patch baked in; that one crashes when the
gestures menu opens, and the check asks for a clean eboot.bin (bbport has its own 60 fps).
the executable and say what is missing (`BB_SKIP_GAME_CHECK=1` skips the check). The other
retail releases (the US `CUSA00900`, the Asian `CUSA03023`, ...) run the same 1.09 executable. Some dumps ship it
with Lance McDonald's 60 fps patch baked in, which crashes when the gestures menu opens: the
check asks for a clean eboot.bin then (bbport has its own 60 fps).
- To build: GCC, CMake, Ninja, Python 3, glslang, SDL3, Vulkan headers and the libraries in
`shell.nix`. With [Nix](https://nixos.org) everything comes from `shell.nix` automatically.
@@ -160,24 +205,18 @@ when several are connected; `BB_GAMEPAD=<GUID or part of the name>`). The keyboa
also next to a connected gamepad (the Steam Deck always has one); both are remapped in the
launcher (*Controls*). The character name is typed on the keyboard in a box over the game.
The touchpad: its left half (Tab, Back/Select) opens the gestures, the right half (Backspace)
the key items.
the key items. There is no PSN: the *Play online / offline* screen is skipped and the game opens
its main menu offline (a game patch; the launcher's *Game effects → Skip the “play online / offline” choice*,
`BB_SKIP_NETWORK_CHOICE=0` shows it). With several monitors, *Screen → Monitor* in the launcher
(`BB_DISPLAY=<number or part of the name>`) picks the one for the game.
Add-ons: `BB_DLC_DIR=/path/to/dlc` (the launcher's *DLC folder*) holds the dumped add-ons of
the game, one folder each (`SPEXPANSIONDLC03/sce_sys/param.sfo`, ...). Their data ships with
the 1.09 update, so a folder with only `sce_sys` is enough: `scripts/content_profile.py` lists
the add-ons of this title into `out/content.bin` and `sceAppContentGetAddcontInfoList` reports
them to the game. The GOTY edition (CUSA03173) needs none.
**Resolution and preset changes:** for outputs other than 1080p (720p on the Steam Deck,
1440p, 4K) the whole game renders at the preset's resolution, set by a patch at start — the
fastest path. Changing the output or the preset in the in-game menu then needs *Apply and
restart the game*. The *Live resolution changes* setting (launcher, in-game menu,
`bbport.ini` `live_resolution=0|1|auto`; **off by default**) instead keeps the game at
1080p internally and scales its render targets at run time, so 720p/1080p/1440p/4K and the
presets switch without a restart. It costs more: the game then believes it renders 1080p
and draws more (e.g. ~8× more small lights), and some targets are copied between sizes —
use it on strong desktop GPUs only (`auto` turns it on for discrete GPUs with 8+ GB that are
not pre-Turing NVIDIA). 1080p output and TAA always use the live path.
**Resolution and preset changes:** the game starts at the scene size of the chosen output
and preset (a patch at start, the fastest path); the output is the monitor's resolution by
default (*Auto*). With *Live resolution changes* (on by default; `bbport.ini`
`live_resolution=0|1`) the preset, the output resolution and the aspect ratio then switch while
playing, to smaller and to larger sizes, at the same FPS as a start at that size; only another
upscaler kind needs a restart. GPUs that cannot give the game's depth a proxy (no D32S8 blits
and no shader stencil export, e.g. GTX 10xx) apply a change at the next start instead.
**TAA:** a separate native-resolution temporal AA mode in the launcher and overlay,
switchable live without an FSR model. The saved FSR preset is restored when returning
@@ -189,20 +228,24 @@ temporal accumulation and leaves its history and HUD unchanged.
**Mods:** the launcher accepts separate loose-file mod folders (with `dvdroot_ps4/`, an extra
wrapper folder, or the game folders such as `chr/` directly; file name case does not matter),
with enable switches and load order. A sibling `CUSA03173-mods/` overlay also works.
The original game is preserved; later mods override conflicting files.
The original game is preserved; later mods override conflicting files. Texture packs with
upscaled map textures (from the mods folder or already in the game folder) get larger graphics
heaps by themselves (`BB_LARGE_HEAPS=1/0` forces it on or off).
**Third-party patches:** shadPS4-format XML patch files in the data directory's `patches/`,
switched on and off in the launcher. See [mods and patches](docs/MODS.md).
switched on and off in the launcher. See [mods and patches](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/MODS.md).
**DLC:** put your add-on dumps in `user/addcont/<title id>/<entitlement label>/` (shadPS4's
layout, e.g. `user/addcont/CUSA03173/SPEXPANSIONDLC03` for The Old Hunters): each folder there is
reported to the game as an installed add-on.
**Launcher language:** Russian, English or Brazilian Portuguese (follows the system language by default).
**Game language:** the PS4 system language, *System language* in the launcher; by default it
follows the desktop's locale (Japanese, English, French, Spanish, German, Italian or Russian,
English for the rest). `run.sh` takes it as `BB_LANGUAGE` (PS4 code: 0 Japanese, 1 English,
2 French, 3 Spanish, 4 German, 5 Italian, 8 Russian; without it, the desktop's locale as above).
**Launcher language:** Russian, English, Brazilian Portuguese, Simplified Chinese or French (follows the system language by default).
The game's own text language is chosen separately, from all 21 languages the game has.
**GPU choice:** *Mode → GPU* picks the graphics card on machines with two (*Auto* prefers the
discrete one; stored by name; `BB_GPU_ID=N` from the shell wins).
**Free camera and game debug menu** (v1.09): enable the corresponding switches in the
launcher or in-game menu and restart. Free camera uses Lance McDonald's
[GoldHEN patch](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
hold Cross and press L3 to cycle modes (keyboard: hold Space and press Z). It needs no fonts
hold Cross and press L3 to cycle modes (keyboard: hold F and press Left Ctrl). It needs no fonts
and conflicts with *Enemy Control*.
For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) into the game's
@@ -210,11 +253,11 @@ For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
the unsafe patch. Open it with the left touchpad / Tab (with the debug menu on, the left half
no longer opens the gestures); Backspace is the right touchpad.
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
pads without a touch surface. The port's settings menu remains Insert / L3+R3.
pads without a touch surface. The port's settings menu remains Insert / holding L3+R3.
GPU occlusion queries still use synthetic pixel counters (`PixelPipeStatDump`), and
`IT_SET_PREDICATION` is unimplemented. Free camera allows visual investigation; it does
not implement GPU occlusion culling.
With the *Restore Debug Camera* patch the mouse turns the free camera. The game's occlusion
queries are Vulkan occlusion queries in the default model (`BB_OCCLUSION_QUERIES=0`: everything
counts as visible, as in the old model).
**Upscaler assets** (not included; FSR 3.1 needs none):
@@ -235,7 +278,8 @@ builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pas
the DLL runs under Proton and is recorded at 20 sizes (progress in the row and in the *Log* tab;
on RDNA4 a second time, for the FP8 variant),
the passes are translated to SPIR-V, and FSR 4.1.1 is selected. The Proton build is picked
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer,
automatically — the first under which the DLL enables FSR 4.1: GE-Proton 10 or newer (also the
AUR's `proton-ge-custom` in `/usr/share/steam/compatibilitytools.d`),
Proton-CachyOS, Proton Experimental (tested: GE-Proton 11, Proton-CachyOS 11, Experimental of
October 2026; Steam's Proton 11.0 and GE-Proton 9 do not). It runs in the Steam runtime it
requires (Steam installs it the first time any game runs with that Proton), or through
@@ -245,6 +289,48 @@ time, ~1.7 GB); the AppImage runs the recording on the system itself, through th
enable FSR 4.1 there, build on a PC and copy the `fsr4_411` folder. The same from the command
line: the launcher's (and the AppImage's) `--build-fsr411 <DLL>`.
**DLSS (NVIDIA GeForce RTX).** NVIDIA's DLSS SDK is not included: check out
[github.com/NVIDIA/DLSS](https://github.com/NVIDIA/DLSS) (`include/` and `lib/Linux_x86_64` are
enough) and build with `DLSS_SDK_ROOT=/path/to/DLSS bash build.sh`. That also builds the bridge
`out/libbbport_dlss.so` (`gpu/dlss_bridge`, the only code that uses the SDK) and copies NVIDIA's
`libnvidia-ngx-dlss.so.<version>` next to `out/bb-probe`; the driver provides the rest. Then pick
*DLSS* in the launcher or the in-game menu: the preset sets the render size, Native AA is DLAA.
DLSS gets the inputs FSR 3.1 gets (scene color, depth, motion vectors, jitter). Without the
libraries, on other GPUs or with `BB_DLSS=0` it is listed as unavailable, and a DLSS setting falls
back to FSR 3.1.
The AppImage built with `DLSS_SDK_ROOT` carries the bridge and NVIDIA's library (NVIDIA's RTX SDK
license allows that in an application; its text is `NVIDIA-DLSS-LICENSE.txt` in the package). A
newer `libnvidia-ngx-dlss.so.<version>` (from the SDK's `lib/Linux_x86_64/rel`) is chosen in the
launcher: *Upscaler → DLSS: NVIDIA's library → Choose a file…*, as the FSR 4.1.1 DLL; it is
copied to `<user>/dlss/`, which the game prefers. A game's `nvngx_dlss.dll` is the Windows
library: NVIDIA's Linux driver does not load it.
**DLSS Frame Generation (GeForce RTX 40 and newer).** The same SDK (310.7 or newer) has NVIDIA's
Linux frame generation library, `lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.<version>`;
`build.sh` copies it next to `bb-probe` with the Super Resolution one, and the launcher's
*DLSS: NVIDIA's library → Choose a file…* takes it too (into `<user>/dlss/`, beside or instead of
the package's). Then *Frame generation → DLSS* in the overlay menu (Insert, or hold L3+R3) or the
launcher: between each two of the game's frames one generated frame is shown (RTX 40), or up to
three (*Multiplier*, RTX 50); the display needs the refresh rate for them. It works with any
upscaler, DLSS or FSR; the inputs are the frame as shown, the frame before its UI, the scene depth,
the motion vectors bbport computes and the game's cameras as clip-space matrices. Without the
library, on an RTX 30 or older or with `BB_DLSS=0` the entry says why it is unavailable, and the
game runs without generated frames. `BB_FRAME_GEN=dlss` and `BB_FRAME_GEN_MULTIPLIER=2..4` set it
from the command line. FSR 3.1 frame generation is offered on every GPU as experimental
(*Frame generation → FSR 3.1*).
**Low latency (NVIDIA Reflex).** On NVIDIA's driver (550 or newer) the port uses the Vulkan
form of Reflex, `VK_NV_low_latency2`, the same mechanism Proton gives Windows games: the game's
thread waits for the driver before each frame so that frames do not queue up in front of the GPU,
and the frame's simulation, submissions and present are marked for the driver. On by default where
the driver has it: the overlay menu (*Output → Low latency (NVIDIA Reflex)*) and the launcher
offer Off, On and On + Boost (`low_latency=off|on|boost` in `bbport.ini`, `BB_LOW_LATENCY`);
on AMD the same setting uses **Anti-Lag 2** (`VK_AMD_anti_lag`: AMD's Windows driver, or Mesa's
anti-lag layer on Linux; `BB_ANTI_LAG=0` leaves it out). With DLSS frame generation it is always
at least On, as NVIDIA requires of games: the generated frames add latency that Reflex takes back.
Reflex and DLSS frame generation came from PR #260 by adds39939.
From source the script takes its tools from the system (MinGW GCC, CMake, Ninja, Python 3,
SPIRV-Tools, Git) or from Nix; the AppImage has them prebuilt.
@@ -264,8 +350,9 @@ the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
game is 16:9; on the 1280×800 screen it gets thin bars).
**Adding the AppImage to Steam** (*Add a Non-Steam Game*) needs no options; the compatibility tool
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage removes it
before its own programs start, so the Steam overlay is not shown in the game.) Where Steam
does not matter. (Steam preloads its overlay into every non-Steam game; the AppImage keeps it out
of the launcher and gives it to the game only, so it can open in the game with Shift+Tab; `BB_STEAM_OVERLAY=0`
leaves it out.) Where Steam
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
AppImage*), set the launch options to
@@ -284,7 +371,9 @@ NVIDIA userspace driver matched to the host kernel module. Standard Linux distri
these libraries under `/usr/lib*`; on NixOS the AppImage's internal `/nix/store` may hide them.
In that case copy the NVIDIA libraries into an accessible directory and set
`BB_NVIDIA_LIB_DIR=/path/to/libraries` (the NVIDIA manifest must also be accessible).
Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
With the host's NVIDIA driver the AppImage also passes the host's EGL vendor files, NVIDIA's
first (`__EGL_VENDOR_LIBRARY_FILENAMES`; without them NVIDIA's ICD gave no `vkCreateInstance` on
some hosts, #107; `BB_NVIDIA_EGL=0`: off). Explicit `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` overrides are preserved. Diagnose drivers inside
the package with:
```bash
@@ -301,8 +390,7 @@ When running from source, install MangoHud separately. A diagnostic launch with
`VK_LOADER_LAYERS_DISABLE=~implicit~` also disables MangoHud.
Useful variables: `BB_FRAME_STATS=1` (frame statistics, including a `Memory:` line: VRAM, GTT,
RSS, images and guest blocks in VRAM), `BB_PC_MODEL=1` (the new memory and translation model,
AMD only; 0, the old model as in 0.3, is the default), `BB_ANISO=N` (anisotropic filtering of scene textures; 16 by
RSS, images and guest blocks in VRAM), `BB_ANISO=N` (anisotropic filtering of scene textures; 16 by
default, 0 = the game's own),
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (how long unused textures / buffers stay in VRAM;
20 / 60), `BB_BREADCRUMBS=0` (no GPU breadcrumbs; with them a GPU hang names the draw or dispatch
@@ -312,13 +400,78 @@ pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4
0 = unbounded), `BB_PRESENT_THREAD=0` (present on the vblank thread, as before),
`BB_LIVE_RES=1` (live resolution changes instead of the startup patch for outputs other than 1080p),
`BB_PAD_RECORD=file` / `BB_PAD_REPLAY=file` (record a route with F9, replay it in scripted tests),
`BB_GC_BUDGET_MB=N` (texture cache budget, as on integrated GPUs), `BB_PRESENT_DUMP_TRIGGER=file`
`BB_GC_BUDGET_MB=N` (texture cache budget, as on integrated GPUs), `BB_VRAM_LIMIT_MB=N` (the VRAM
budget taken as at most N MiB: checks the behaviour of cards with little VRAM), `BB_ASYNC_PIPELINES=0`
(new pipelines compile at once and the game waits; by default in the background), `BB_PRESENT_DUMP_TRIGGER=file`
with `BB_PRESENT_DUMP_COUNT=N` (dump N consecutive presented frames),
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (FSR 4.1.1 variant; by default FP8 where the GPU has FP8
matrices), `BB_READBACKS=0|1|2` (reads of GPU-written memory by the game: 1 by default, 2
precise and slow, 0 off).
More in [docs/](docs); recent changes: [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md) (in Russian),
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
precise and slow, 0 off), `BB_RENDER_RESOURCE_CACHE_MB=160` (the game's render resource cache
at its own size instead of 512 MiB of the port's memory; `BB_DMEM_EXTRA_MB`, 512 by default, is
the direct memory the port keeps past the game's own). All of them, each with what it does:
[docs/ENVIRONMENT.md](docs/ENVIRONMENT.md); the log's `Environment:` line names those set and any
unknown `BB_*` name (a typo). More in [docs/](docs); recent changes: [docs/CHANGES_0.7.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.7.md),
[docs/CHANGES_0.6.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CHANGES_0.6.md).
**Online play.** A separate module, `out/gpu/libbbnet20.so` (`gpu/bbnet20`: shadPS4 v0.20's
network, NP and shadNet code, the port's BBSC and The Hunter's Dream backend), built when
protobuf, nlohmann_json, OpenSSL, zlib and miniupnpc are there (`shell.nix`) and included in the
AppImage. The launcher's *Online* page turns it on (`BB_ONLINE=1`) and offers two pools:
- **Seamless Hunt** (default): a shadNet server with BBSC's game server
(`play.seamlesshunt.com:31313`, WebAPI `:31315`); bells, summons, messages, bloodstains and
**seamless co-op (BBSC)**: the session survives deaths, lamps, area changes, bosses and the
Hunter's Dream, up to five hunters (*Seamless co-op (BBSC)*, on by default; every player needs
it and the same server). The account (online id and password) is made on
https://seamlesshunt.com. Details: [docs/BBSC.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/BBSC.md).
- **The Hunter's Dream** (thehuntersdream.com): sign in with Discord from the launcher (a code is
confirmed on the site); no BBSC.
On Windows the module is `online\bbnet20.dll` (Seamless Hunt with BBSC; the launcher's *Online*
section). The Linux folder package keeps it open as `online/libbbnet20.so`, and `BB_NET_MODULE`
points the port at any other build of it (a newer BBSC, say).
The former module on shadp2p (`libbbnet.so`) was removed (0.7). shadPS4's
public server (`srv.shadps4.net`) accepts only shadPS4's own builds. Offline the module is never
loaded; a build without it plays offline only.
## Windows
Since 0.7 bbport runs natively on Windows 10/11 (x86-64, a Vulkan 1.3 GPU with a current driver:
AMD Adrenalin, NVIDIA GeForce, Intel Arc). The same code as on Linux: the game's x86-64 code runs
directly, the runtime's POSIX layer is implemented on Win32 (`gpu/platform/win32`), and the
game's memory is shared with Vulkan through `VK_EXT_external_memory_host`.
**Package:** `Bloodborne-bbport-<version>-windows-x64.zip`. Unpack it anywhere (not into *Program Files*), start
`Bloodborne.exe`, choose the game folder (the one with `eboot.bin`), press *Play*. Insert (or
holding L3+R3) opens the port's menu in the game. Everything the port writes stays in that folder:
`bbport.ini` (settings), `user\` (saves, shader cache), `out\` (prepared game files), `logs\` (one
log per run: for a report send the newest one). `Bloodborne.exe --play` starts the game without
the launcher window (through `bbport-run.exe`).
**Launcher:** WPF in the Windows 11 style, following the system's light or dark theme, in five
languages; the same settings as on Linux (sections with descriptions), monitor, controller and
GPU choice, the log of the running game, *Stop*.
**Upscalers:** FSR 3.1 and FSR 4 as on Linux. FSR 4.1.1: choose *Build from an AMD DLL…* and pick
`amd_fidelityfx_upscaler_dx12.dll` 4.1.x; the recording runs on vkd3d-proton and DXVK from the
package (`tools\fsr4cap\bin`), so any GPU with Vulkan can build the set (a minute or two, into
`fsr4_411\`). FSR 4.1.2's models (INT8 for RDNA2/3, *Lite*): pick the driver's `amdxcffx64.dll`
(Adrenalin 26.10.1 or newer, `C:\Windows\System32\DriverStore\FileRepository\u0*\`) together with
the upscaler DLL. DLSS and DLSS Frame Generation (GeForce RTX): with `bin\bbport_dlss.dll` and
NVIDIA's `nvngx_dlss.dll` / `nvngx_dlssg.dll` in the package.
**NVIDIA:** at start the port adds a *prefer maximum performance* power profile for
`bb-probe.exe` to the driver, unless you chose a mode in NVIDIA's Control Panel yourself
(`BB_NV_POWER=0`: leave it alone); presentation uses FIFO (`BB_NVIDIA_MAILBOX=1`: Mailbox).
**Online play:** the launcher's *Online* section, the same two pools as on Linux (Seamless Hunt
with BBSC, The Hunter's Dream with Discord sign-in), UPnP and a connection check; the module is
`online\bbnet20.dll`. **Mods:** the launcher's *Mods and patches* section, as on Linux.
**Building:** in MSYS2's CLANG64 shell (`mingw-w64-clang-x86_64` toolchain, CMake, Ninja, Python,
SDL3, glslang), or cross-built from Linux with llvm-mingw; the package
is made by `packaging/windows/package.sh` (the launcher needs the .NET 9 SDK). DLSS:
`DLSS_SDK_ROOT=<NVIDIA's DLSS SDK for Windows>` (`sdk/DLSS-windows`).
## Repository layout
@@ -326,45 +479,71 @@ More in [docs/](docs); recent changes: [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md
|---|---|
| `src/` | Loader (`probe.c`) and the HLE runtime |
| `scripts/` | Offline preparation of the game image, module linking, patch compiler |
| `gpu/` | Renderer library: vendored shadPS4 video core with this port's changes (`gpu/VENDOR.txt`), shims, ImGui menu, FSR 4.1.1 runtime (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
| `launcher/`, `packaging/` | GTK4 launcher; Nix package and AppImage |
| `gpu/` | Renderer library, the port's own (`gpu/VENDOR.txt`): command processor, shader translator, renderer, caches, ImGui menu, FSR 4.1.1 runtime (`gpu/upscale/fsr411`); the online module (`gpu/bbnet20`) |
| `launcher/`, `packaging/` | GTK4 launcher (Linux), WPF launcher (`launcher/windows`), the shared settings schema; Nix package, AppImage, Windows package |
| `patches/` | Community patches for Bloodborne |
| `tools/` | Developer tools: scripted runs, A/B toggles, FSR benchmark helpers, FSR 4 shader rewrites, `fsr4cap` (FSR 4.1.1 recording/extraction) |
| `tests/` | Loader, runtime, patch and renderer tests |
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](docs/motion_vectors.md), [roadmap](docs/ROADMAP.md)) |
| `docs/` | Design notes and measurements ([upscaler](docs/upscaler.md), [parallel GPU](docs/parallel_gpu.md), [motion vectors](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/motion_vectors.md), [roadmap](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/ROADMAP.md)) |
Tests: `bash build.sh --test`, `python3 -m unittest discover -s tests`, and
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test camera-matrices-test`.
## Roadmap
- The new memory and translation model on by default once it is stable, NVIDIA included; the
old model is removed after that.
- The GPU reads resource descriptors itself (bindless), textures are objects created at load
time: no shader code or constant engine executed on the CPU, no texture cache keyed by
address.
- Shaders translated ahead of time, at install, not during play.
- NVIDIA on Windows: more reports from RTX cards in the heaviest views.
- Textures as objects created at load time (less guessing by address, uploads outside the
frame).
- All of the game's shaders translated ahead of time (today: recipes from players' caches).
- More CPU parallelism in GPU command processing (split the draw-recording stage further),
scaling to all hardware threads — most important for the Steam Deck.
- Async compute for the upscaler (the frame is GPU-bound at 4K).
- XeSS (super resolution) and XeFG frame generation through a Wine helper sharing Vulkan
memory (a memory-bridge prototype is in `tools/bridge_helper`); DLSS for NVIDIA users;
inputs exposed so that OptiScaler-style mapping works.
- Frame generation (FSR 3.1 FG first), reactive and transparency masks for particles and fog.
memory (a memory-bridge prototype is in `tools/bridge_helper`); inputs exposed so that OptiScaler-style mapping works.
- Reactive and transparency masks for particles and fog.
- Fix the races in AMD's FSR 4.1.1 shaders at output widths that are not multiples of 64
(e.g. 1600×900), as already done for the left-edge race in FSR 4 v07 at 1080p.
- Steam Deck validation of the AppImage; HDR output.
## Related projects
- [ducvd89/Bloodborne-Android](https://github.com/ducvd89/Bloodborne-Android): Bloodborne on
Android, a fork of bbport (GSR2 / FSR 3.1 and LSFG, an on-screen controller; tested on
Snapdragon 8 Gen 2 and 8 Elite).
- [bloodborne-native/bloodborne-mac](https://github.com/bloodborne-native/bloodborne-mac):
Bloodborne running natively on Apple Silicon, a macOS app built on bbport and shadPS4.
## Credits and licenses
bbport is licensed under the **GNU GPL v2 or later** ([LICENSE](LICENSE)) — it contains code
from shadPS4 (GPL-2.0-or-later). Third-party components keep their licenses:
[shadPS4](https://github.com/shadps4-emu/shadPS4) video core and shader recompiler (GPL-2.0+),
from shadPS4 (GPL-2.0-or-later). Reusing bbport's code: keep its copyright and license notices
(`SPDX-FileCopyrightText: Copyright 2026 bbport contributors` at the top of its files) and
[NOTICE](NOTICE), and say where the code comes from, with a link to this repository. Third-party components keep their licenses:
[shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+: the renderer and shader translator were derived from it and rewritten; the online module keeps its network and NP code),
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn (MIT; FSR 3.1 on Vulkan and the
FSR 4 v07 provider), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), DejaVu fonts,
[BBSC](https://github.com/CLSAMG22/BBSC) seamless co-op (GPL-2.0+, in the online module), the
online module's network code from [shadPS4 v0.20](https://github.com/shadps4-emu/shadPS4/tree/v.0.20.0),
The Hunter's Dream backend from
[droogie/bbhost](https://github.com/droogie/bbhost) (GPL-3.0-or-later, so `libbbnet20.so` as a
whole is GPL-3.0-or-later),
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, used to build the
FSR 4.1.1 assets). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
community members (`patches/Bloodborne.xml`). AMD's FSR 4 DLLs and model data are not
distributed here.
FSR 4.1.1 assets), [wepoll](https://github.com/piscisaureus/wepoll) (BSD-2-Clause, the Windows
online module). Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and other
community members (`patches/Bloodborne.xml`). The DLSS bridge (`gpu/dlss_bridge`, MIT) is adapted
from [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
AMD's FSR 4 DLLs and model data and NVIDIA's DLSS SDK and libraries are not distributed here.
Contributions merged into bbport (thank you; the full list with each release is in
[docs/CONTRIBUTORS_0.7.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CONTRIBUTORS_0.7.md)): SrDeTs (#10), IgorWarzocha (#36), itachi-re
(#43), Bainianzzz (#71), hideoneric (#78), guitaripod (#80), unTanana (#83, #84, #89), andrR89
(#93), SaeedShow (#108), AndiMajore (#112, #113), ayozetr (#117), JuanraCM (#120), Supermedo and Awaken (the online
module's network stack, from Supermedo's Windows fork), wheat32 (#360 larger heaps for texture packs),
Sarxzer (#347 French), whp199 (#210, #211, #214, #342, #343), DominickCobb-rs (#346), maiconjsv
(#339), iannitram (#293), adds39939 (#260 DLSS Frame Generation and Reflex), AndiMajore (#166,
#224, #225), Qiming-Liu (#75, #157 Chinese), AyrtonSanabio (#153, #154), axpico (#206), msisovic (#193),
noelmiller (#235), Obrikash (#140), Aryxa (#137), ArenaMMORPG (#352/#353), guutol (trophies),
redditor69 (GT-VBAO), SrShadowy and Teefpyre (engine and BBSC fixes from SrShadowy's fork), and
the players who sent their shader caches and logs.
-347
View File
@@ -1,347 +0,0 @@
ДАННЫЙ ПРОЕКТ НЕ ИМЕЕТ ОТНОШЕНИЯ К SHADPS4. ВСЕ ВОПРОСЫ, СВЯЗАННЫЕ С ЭТИМ ПРОЕКТОМ - ПИШИТЕ НА ДИСКОРД СЕРВЕР https://discord.gg/KYZRKk9CB, А НЕ НА СЕРВЕР SHADPS4
# bbport — нативный порт Bloodborne для Linux
> README оригинального проекта (deadinside28/bloodborne_pc), сохранён ради технических
> подробностей. README этого форка — [README.md](README.md) (на английском).
[English](BBPORT.md) · **Русский**
bbport — это аналог Wine + DXVK, только для одной игры: *Bloodborne* для PlayStation 4
(CUSA03173, версия игры 1.09) на ПК с Linux x86-64. Оригинальный исполняемый файл игры
запускается прямо на ПК:
- **как в Wine**, x86-64-код игры выполняется на процессоре напрямую, а системные библиотеки PS4
заменяет собственный рантайм, написанный под эту игру;
- **как в DXVK**, графика игры переводится в Vulkan — рендером на основе
[shadPS4](https://github.com/shadps4-emu/shadPS4), сильно доработанным под Bloodborne, вплоть
до временного апскейлинга AMD FSR 3.1, FSR 4 и FSR 4.1.1;
- память и работа видеочипа переезжают на модель ПК: в лаунчере есть экспериментальный режим
*Новая модель памяти и трансляции* (только видеокарты AMD); без него игра работает со старой
моделью памяти, как в 0.3.
До полной модели Wine + DXVK осталось два шага: довести новый режим до режима по умолчанию и
перенести чтение дескрипторов ресурсов (текстур, буферов) с процессора на видеочип (см. ниже).
> **Файлов игры в репозитории нет.** Нужен свой дамп Bloodborne (CUSA03173, v1.09).
> Проект не связан с Sony Interactive Entertainment, FromSoftware и AMD.
**Состояние: экспериментальный, играбельный.** Игра запускается, загружает сохранения и
проходится (Сон Охотника и несколько локаций Ярнама) со звуком,
геймпадом и сохранением. Полное прохождение не проверено; основательно проверена одна машина
(Linux, AMD Radeon RX 7800 XT, Mesa/RADV).
## Что сделано
- **Нативное исполнение.** eboot заранее превращается в плоский образ памяти, в который как
нативный код прилинкованы libc и libSceFios2 из PS4. Никакой эмуляции процессора и
трансляции инструкций: код игры работает на полной скорости.
- **Два режима работы** (лаунчер → *Режим работы*):
- **Переключатель выключен (по умолчанию) — как в 0.3.** Старая модель памяти: копии памяти
игры в VRAM, записи отслеживаются защитой страниц. Со всеми исправлениями после 0.3
(векторы движения и апскейлер, мерцание при включённом DoF, повреждённый кэш шейдеров).
- **Новая модель памяти и трансляции — экспериментальная, только видеокарты AMD.** Память
игры находится в оперативной памяти, и GPU читает её на месте, как буферы игры для ПК; то,
что он читает часто, держится в видеопамяти и освобождается, когда перестаёт использоваться
(текстуры — через 20 с, буферы — через 60 с). Нет отслеживания записей и копий всей памяти,
видимой GPU. Работа командного процессора PS4 переводится в команды Vulkan, а не
эмулируется на процессоре: записи в память (`WRITE_DATA`, DMA) выполняет видеочип в порядке
потока команд, метки (fences) пишутся, когда видеочип действительно закончил работу.
Где игра упирается в процессор, FPS выше на 20–25 % (замер на месте в Сне Охотника), рывков
меньше. **Может вылетать**; основательно проверена только на ПК автора (RX 7800 XT). На
NVIDIA и Intel переключатель недоступен: драйвер NVIDIA не отображает память игры нужным
образом, Intel не проверялся. Без лаунчера — `BB_PC_MODEL=1`; попробовать на другой
видеокарте — `BB_PC_MODEL_ANY_GPU=1`.
Неиспользуемые текстуры освобождаются в обоих режимах, поэтому видеопамять больше не растёт
с каждой локацией.
- **Разблокированная частота кадров.** Патчи сообщества (`patches/Bloodborne.xml`) переводят
симуляцию на реальное время кадра; ~90 FPS в 4K с FSR 4 Balanced на RX 7800 XT, ~150 FPS в
1440p с FSR 4 Quality. Есть режимы 30/60/90 FPS.
- **Временной апскейлинг под эту игру.** Буфера скоростей в Bloodborne нет, поэтому порт
считает векторы движения сам: движение камеры — по глубине и матрицам сцены, движение
объектов (персонажи, ткань, оружие) — по позициям вершин прошлого кадра. Сцена сдвигается
субпиксельным jitter (Halton) и рисуется в уменьшенном разрешении, апскейлер достраивает
вывод (720p для Steam Deck, 1080p, 1440p или 2160p), интерфейс рисуется сразу в выходном разрешении.
- **FSR 3.1** (FireBurn/FSR-Vulkan).
- **FSR 4 (INT8, модель v07)** на видеокартах с нужными возможностями шейдеров Vulkan,
включая RDNA2/3 (см. требования).
- **FSR 4.1.1**: DLL AMD 4.1.1 один раз записывается под vkd3d-proton, а её проходы
воспроизводятся нативно в Vulkan; результат **побитово совпадает** с DLL. Два варианта, как
в самой DLL: INT8 на любой видеокарте с нужными возможностями шейдеров и матрицы FP8 на
RDNA4 (RX 9000; выбирается сам). Ассеты собираются на твоей машине из одной твоей DLL
4.1.x: в лаунчере кнопка *FSR 4.1.1 из своей DLL AMD → Выбрать DLL…* (2–5 минут, на RDNA4
вдвое дольше; нужен свежий Proton), или `tools/fsr4cap`.
- Быстрее шейдеров самой AMD на RDNA3: финальные проходы FSR 4 и 4.1.1 переписаны с записью
через shared-память (в 3.5 и 2.3 раза быстрее, побитово так же); FSR 4 в 4K на RX 7800 XT
стоит ~4 мс вместо ~6.
- **Многопоточная обработка GPU-команд.** Поток команд PS4 разбирается в одном потоке, а
привязка ресурсов и запись draw — в другом (двухстадийный конвейер), плюс отдельный поток
записи команд Vulkan и вспомогательные потоки для копий памяти. В начале проекта единственный
поток GPU ограничивал игру ~26 FPS; сейчас 90–150 FPS в зависимости от разрешения и сцены.
- **Меню в игре** (Insert или L3+R3): апскейлер, пресет, резкость, разрешение вывода, эффекты
игры (аберрация, DoF, размытие в движении, SSAO, сглаживание игры, SSR, детализация моделей).
Открывается там, где его оставили, курсор мыши над ним виден.
- **Лаунчер на GTK4** и **AppImage** для Steam Deck.
## Чем отличается от shadPS4
| | shadPS4 | bbport |
|---|---|---|
| Охват | Универсальный эмулятор PS4, много игр | Одна игра: Bloodborne v1.09 |
| Загрузка | Свой ELF-загрузчик и эмуляция ядра во время работы | eboot заранее превращается (`scripts/`) в образ с прилинкованными libc/Fios2 PS4; загрузчик на C отображает его и передаёт управление игре (загрузчик и рантайм — ~5 тыс. строк) |
| Память | Вид памяти PS4 со стороны GPU хранится копиями в VRAM, синхронизация через отслеживание записей защитой страниц | По умолчанию та же модель (как в 0.3); в режиме *Новая модель памяти и трансляции* (AMD): память игры в оперативной памяти, GPU использует её на месте, часто читаемое — в VRAM, освобождается, когда не нужно |
| Командный процессор | Эмулируется: записи в память, DMA и метки выполняет процессор во время разбора команд | По умолчанию так же; в новом режиме переводится в команды Vulkan, их выполняет видеочип в порядке потока, метки — после реального завершения работы |
| Системные библиотеки | Широкий HLE ОС PS4 | Небольшой рантайм (`src/runtime_*.c`) ровно под то, что вызывает Bloodborne: память, потоки, синхронизация, файлы, звук (включая ATRAC9), геймпад, сохранения, AppContent |
| GPU | Видеоядро и рекомпилятор шейдеров shadPS4 | То же ядро (вложено, GPL) с ~200 помеченными правками (`bbport:`) и новыми модулями: двухстадийный конвейер draw, мемоизация состояния прохода и наборов текстур, уменьшенные цели сцены, векторы движения, FSR 3.1/4/4.1.1, захват кадра, профайлер GPU |
| Поток GPU | Один поток обрабатывает весь поток команд (узкое место в Bloodborne) | Разбор и запись draw идут в разных потоках; работа масштабируется под число потоков (в том числе Steam Deck) |
| Апскейлинг | — | Временной (FSR 3.1, FSR 4, FSR 4.1.1) на собственных векторах движения и jitter игры |
| Патчи игры | Патч-файлы применяет эмулятор | Те же патчи сообщества компилируются при запуске (`scripts/patches.py`); разрешение рендера, эффекты и FPS — из лаунчера |
Без shadPS4 bbport бы не было: его рендерер и рекомпилятор шейдеров — основа графической части.
### Wine + DXVK для одной игры
- **Процессор.** Процессор PS4 — x86-64, поэтому код игры выполняется прямо на процессоре ПК, без
эмуляции и перевода инструкций.
- **Системные библиотеки.** Как Wine подменяет Windows API, так рантайм bbport (`src/runtime_*.c`)
реализует ровно те функции ОС PS4, которые вызывает Bloodborne: память, потоки, файлы, звук,
геймпад, сохранения.
- **Память.** В новом режиме — как у игры для ПК: данные игры в оперативной памяти, видеопамять
используется так, как её использует игра для ПК. Пока режим экспериментальный и работает только
на видеокартах AMD.
- **Графика.** Поток команд видеочипа PS4 (PM4) — это записанные вызовы графического API игры:
его пишут 99 функций статически слинкованной libGnm, так что разбор потока и перевод самих
вызовов дают одно и то же. Шейдеры GCN переводятся в SPIR-V. В новом режиме работа командного
процессора (записи в память, DMA, метки) тоже переводится в команды Vulkan. По-старому
осталось одно: дескрипторы ресурсов (текстур, буферов) транслятор читает из памяти игры на
процессоре, а текстуры узнаёт по адресу. Следующий большой шаг — чтение дескрипторов
видеочипом (bindless) и текстуры как объекты, которые создаются при загрузке.
«Порт» здесь — сборка под одну игру, а не переписанный исходный код: исходного кода игры у
проекта нет, и он его не содержит.
## Требования
- Linux x86-64, видеокарта с Vulkan 1.3. Проверено: AMD RX 7800 XT, Mesa 26 (RADV).
Режим *Новая модель памяти и трансляции* — только видеокарты AMD.
FSR 4 / 4.1.1 требуют shader Float16, Int8/Int16, целочисленных dot-произведений,
linear compute derivatives и extended storage image formats; FSR 4.1.1 дополнительно
требует `VK_VALVE_shader_mixed_float_dot_product`. Неподдерживаемый вариант заменяется
на FSR 3.1 до первого кадра и недоступен в игровом меню.
- Свой расшифрованный дамп игры: папка `CUSA03173` (eboot.bin, sce_module, ...), версия 1.09.
Дамп обновления — отдельная папка: скопируйте её поверх базовой игры с заменой файлов. Одна
базовая игра (1.00) падает при запуске (guest offset 0x20348b8); лаунчер и `run.sh` проверяют
исполняемый файл и пишут, чего не хватает (`BB_SKIP_GAME_CHECK=1` отключает проверку).
- Для сборки: GCC, CMake, Ninja, Python 3, glslang, SDL3, заголовки Vulkan и библиотеки из
`shell.nix`. С [Nix](https://nixos.org) всё берётся из `shell.nix` автоматически.
## Сборка и запуск
```bash
git clone --recursive <этот репозиторий> bbport && cd bbport
bash build.sh # собирает out/bb-probe и out/gpu/libbbgpu.so
BB_GAME_DIR=/путь/к/CUSA03173 bash run.sh
```
или через лаунчер (выбрать папку игры, настройки, «Запустить»):
```bash
bash launcher/bb-launcher.sh # launcher/install-desktop.sh добавит его в меню приложений
```
По умолчанию папка игры ищется рядом с репозиторием (`../CUSA03173`). Сохранения и кэш шейдеров
— в `user/` (в лаунчере можно выбрать другую папку), настройки — в `bbport.ini`. Геймпад через
SDL3 (если их несколько, нужный выбирается в лаунчере: *Управление → Контроллер*;
`BB_GAMEPAD=<GUID или часть имени>`). Клавиатура тоже работает, и при подключённом геймпаде
(на Steam Deck он есть всегда); обе раскладки меняются в лаунчере (*Управление*). Имя персонажа
вводится с клавиатуры в окне поверх игры. Тачпад: левая половина (Tab, Back/Select) открывает
жесты, правая (Backspace) — личные вещи.
**Смена разрешения и пресетов:** для вывода не 1080p (720p на Steam Deck, 1440p, 4K) вся игра
рисуется в разрешении пресета, которое задаёт патч при запуске, — это самый быстрый путь.
Смена вывода или пресета в игровом меню тогда требует «Применить и перезапустить игру».
Настройка «Смена разрешения на лету» (лаунчер, игровое меню, `bbport.ini`
`live_resolution=0|1|auto`; **по умолчанию выключена**) вместо этого оставляет игре 1080p и
масштабирует её цели на ходу: 720p/1080p/1440p/4K и пресеты меняются без перезапуска. Это
дороже: игра считает, что рисует 1080p, и рисует больше (например, мелких источников света
примерно в 8 раз больше), часть целей копируется между размерами — включай только на мощных
настольных видеокартах (`auto` включает её на дискретных GPU с 8+ ГБ, кроме NVIDIA старше
Turing). Вывод 1080p и TAA всегда идут живым путём.
**TAA:** отдельный пункт сглаживания в лаунчере и игровом меню, переключается на ходу.
Работает в разрешении вывода без модели FSR; пресет FSR сохраняется для обратного
переключения. Native AA у FSR выполняет реконструкцию поверх полного разрешения и
может быть тяжелее выключенного сглаживания. TAA тоже добавляет обработку кадра.
**Моды:** лаунчер подключает распакованные моды из отдельных папок (с `dvdroot_ps4/`, с лишней
папкой-обёрткой или сразу с папками игры вроде `chr/`; регистр имён не важен), с переключателями
и порядком загрузки. **Сторонние патчи:** XML-файлы в формате shadPS4 в папке `patches/` каталога
данных, включаются в лаунчере. Подробнее — [моды и патчи](docs/MODS.md).
**Язык лаунчера:** русский или английский (по умолчанию — как в системе).
**Свободная камера и debug menu игры** (v1.09): включи соответствующие галочки в лаунчере
или игровом меню и перезапусти игру. Для камеры используется
[патч Lance McDonald из GoldHEN](https://github.com/GoldHEN/GoldHEN_Patch_Repository/blob/main/patches/xml/Bloodborne-Orbis.xml):
удерживай Cross и нажимай L3 для переключения режимов (клавиатура: Space + Z).
Камере шрифты не нужны; с патчем Enemy Control она несовместима.
Для debug menu сначала установи `DbgFont14h.ccm` и `DbgFont14h.tpf` из
[Debug Menu and XML Patch](https://www.nexusmods.com/bloodborne/mods/253) в
`dvdroot_ps4/font/` внутри папки игры. Если файлов нет или они пустые, запуск остановится
с объяснением. Меню открывается левым touchpad / Tab (с включённым debug menu левая половина
больше не открывает жесты); правый touchpad — Backspace.
Координаты касания передаются с SDL-геймпадов; Back/Select заменяет левый клик на геймпадах
без сенсорной поверхности. Меню настроек порта по-прежнему открывается Insert / L3+R3.
GPU-запросы occlusion пока возвращают искусственные счётчики пикселей
(`PixelPipeStatDump`), а `IT_SET_PREDICATION` не реализован. Свободная камера позволяет
проверять видимость, но не добавляет GPU occlusion culling.
**Ассеты апскейлеров** (в репозитории их нет; FSR 3.1 они не нужны):
```bash
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, собраны Q2RTX из исходников AMD)
# FSR 4.1.1 — из своих DLL AMD (например, FSR4_LATEST у OptiScaler), нужен GE-Proton 10 или новее:
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
```
**FSR 4.1.1 из своей DLL.** Проще всего — в лаунчере: *Апскейлер → FSR 4.1.1 из своей DLL AMD →
Выбрать DLL…* и указать `amd_fidelityfx_upscaler_dx12.dll` версии 4.1.x: из папки `FSR4_LATEST`
OptiScaler или из игры с FSR 4.1. Этого одного файла достаточно: DLL AMD сама экспортирует
FidelityFX API, загрузчик не нужен (через `amd_fidelityfx_loader_dx12.dll` записывалась бы только
DLL без этих функций, и тогда лаунчер его спросит). DLL проверяется сразу, без долгой записи, и не подходят: официальная FSR 4.0.x
(например, 4.0.3 из Pragmata — AMD включает её только на RDNA4, под Proton на других видеокартах
она не запускается) и сборки сообщества (4.0.2b, 4.1.1b и подобные INT8-сборки OptiScaler:
другая модель и число проходов). Дальше DLL запускается под Proton и записывается в 20
разрешениях (ход виден в строке и во вкладке «Журнал»; на RDNA4 — второй раз, для варианта FP8), проходы переводятся в SPIR-V, и FSR 4.1.1
сразу выбирается. Proton выбирается сам — первый, под которым DLL включает FSR 4.1: GE-Proton 10
или новее, Proton-CachyOS, Proton Experimental (проверено: GE-Proton 11, Proton-CachyOS 11,
Experimental от октября 2026; Proton 11.0 из Steam и GE-Proton 9 — нет). Он запускается в
рантайме Steam, который требует (Steam ставит его при первом запуске любой игры с этим Proton),
или через umu-launcher, если он установлен. На NixOS umu-launcher берётся из `nix-shell` (в первый
раз скачивается, ~1,7 ГБ); AppImage запускает запись на самой системе, через systemd пользователя
(`systemd-run --user`), так как системный `/nix` ей не виден. Запись на Steam Deck пока не проверена; если DLL не включит
там FSR 4.1, собери на ПК и скопируй папку `fsr4_411`. То же из командной строки:
`--build-fsr411 <DLL>` у лаунчера (и у AppImage).
Скрипт из исходников берёт инструменты из системы (MinGW GCC, CMake, Ninja, Python 3,
SPIRV-Tools, Git) или из Nix; в AppImage они уже собраны.
**FP8 на RDNA4.** Когда vkd3d-proton даёт кооперативные матрицы FP8 (RDNA4), DLL запускает
другой вариант своих проходов: другие шейдеры и веса, другие размеры диспатчей. Сборка
записывает оба: INT8 в `fsr4_411/`, FP8 в `fsr4_411/fp8/`, а игра сама выбирает FP8, если у
видеокарты есть матрицы FP8 (`VK_EXT_shader_float8`); в журнале — `Upscaler: FSR 4.1.1 replay,
FP8 …`. `BB_FSR411_VARIANT=int8` — принудительно INT8. Проверено на RX 7800 XT через эмуляцию FP8
на FP16 в vkd3d-proton (`BB_FSR4CAP_FP8=1` записывает его и там, `fp8emu/`): побитово как DLL;
**на самой RDNA4 ещё не запускалось.**
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
`dist/Bloodborne-bbport-x86_64.AppImage`; данные в `~/.local/share/bbport`, `--play` запускает
игру без окна лаунчера (игровой режим). Модели FSR 4.1.1 в пакет не входят: собери их кнопкой
лаунчера (см. выше), они лягут в `~/.local/share/bbport/fsr4_411` (`BB_PACKAGE_FSR411=1` кладёт
локальную `fsr4_411` в AppImage для своих устройств). На Steam Deck выбирай вывод 1280×720 (игра 16:9, на
экране 1280×800 будут узкие полосы).
**Добавление AppImage в Steam** («Добавить стороннюю игру»): ничего настраивать не нужно,
инструмент совместимости не важен. (Steam подгружает свой оверлей во все сторонние игры; AppImage
убирает его до запуска своих программ, поэтому оверлея Steam в игре нет.) Если Steam запускает игры без FUSE (NixOS: FHS-песочница Steam; AppImage тогда завершается с
*Cannot mount AppImage*), в параметрах запуска укажи
```
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
```
Тогда AppImage при первом запуске распакуется (~2 ГБ, `~/.cache/appimage_extracted_*`, ~10 с),
а дальше будет запускать эту копию; новая версия AppImage распакуется заново, старую копию можно
удалить. Без `TMPDIR` распаковка шла бы в `/tmp` Steam, а он там в оперативной памяти.
` --play` после `%command%` — запуск без лаунчера.
**NVIDIA в AppImage:** пакет автоматически ищет установленный на машине 64-битный NVIDIA
Vulkan-драйвер и подключает его вместе с комплектными AMD/Intel-драйверами. Используются
библиотеки NVIDIA с этой машины, чтобы их версия совпадала с модулем ядра. На обычных Linux
дистрибутивах они доступны в `/usr/lib*`; на NixOS внутренний `/nix/store` AppImage может скрывать
драйвер хоста. В этом случае библиотеки NVIDIA нужно скопировать в доступную папку и указать
`BB_NVIDIA_LIB_DIR=/путь/к/библиотекам` (манифест NVIDIA тоже должен быть доступен).
Явные `VK_DRIVER_FILES`/`VK_ICD_FILENAMES` сохраняются. Проверка драйвера внутри пакета:
```bash
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
```
Пользователь подтвердил запуск с FSR 3 на GTX 1060 6GB (Fedora 44, NVIDIA 580.178.04);
выбор FSR 4 приводил к чёрному окну. На этой конфигурации используй FSR 3.
MangoHud включён в AppImage; включи его галочку в лаунчере. Если MangoHud установлен ещё и в
системе или включён оверлей производительности Steam (игровой режим Steam Deck), рисуется только
один оверлей (два давали двоящийся текст со сдвигом).
При запуске из исходников MangoHud нужно установить отдельно. Диагностический запуск
с `VK_LOADER_LAYERS_DISABLE=~implicit~` отключает и MangoHud.
Полезные переменные: `BB_FRAME_STATS=1` (статистика кадров, в том числе строка `Memory:` —
VRAM, GTT, RSS, текстуры и блоки памяти игры в VRAM), `BB_PC_MODEL=1` (новая модель памяти и трансляции,
только AMD; по умолчанию 0 — старая модель, как в 0.3), `BB_ANISO=N` (анизотропная фильтрация текстур сцены; по умолчанию
16, 0 — как в игре),
`BB_GC_IDLE_SECONDS=N` / `BB_VRAM_IDLE_SECONDS=N` (сколько неиспользуемые текстуры / буферы
остаются в VRAM; 20 / 60), `BB_BREADCRUMBS=0` (без меток GPU; с ними зависание видеокарты
называет отрисовку или dispatch, на которой она встала), `BB_GPU_PROFILE=1` (время GPU по
проходам), `BB_FSR4_PROFILE=1` (время каждого прохода FSR 4), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
`BB_FRAMES_AHEAD=N` (на сколько кадров поток команд может опережать GPU; по умолчанию 1, 0 — без
ограничения), `BB_PRESENT_THREAD=0` (показ в потоке vblank, как раньше), `BB_LIVE_RES=1` (смена
разрешения без перезапуска вместо патча при запуске для вывода не 1080p),
`BB_PAD_RECORD=файл` / `BB_PAD_REPLAY=файл` (запись маршрута по F9 и его повтор в тестах),
`BB_GC_BUDGET_MB=N` (бюджет кэша текстур, как на встроенных GPU), `BB_PRESENT_DUMP_TRIGGER=файл`
с `BB_PRESENT_DUMP_COUNT=N` (снимок N показанных кадров подряд),
`BB_FSR411_VARIANT=int8|fp8|fp8emu` (вариант FSR 4.1.1; по умолчанию FP8, где у видеокарты есть
матрицы FP8), `BB_READBACKS=0|1|2` (чтение игрой памяти, записанной GPU: по умолчанию 1, 2 —
точно и медленно, 0 — выключено).
Подробнее — в [docs/](docs); последние изменения — [docs/CHANGES_0.4.md](docs/CHANGES_0.4.md),
[docs/CHANGES_2026-10-06.md](docs/CHANGES_2026-10-06.md).
## Устройство репозитория
| Путь | Содержимое |
|---|---|
| `src/` | Загрузчик (`probe.c`) и HLE-рантайм |
| `scripts/` | Подготовка образа игры, линковка модулей, компилятор патчей |
| `gpu/` | Библиотека рендерера: видеоядро shadPS4 с правками порта (`gpu/VENDOR.txt`), прослойки, меню ImGui, рантайм FSR 4.1.1 (`gpu/shadps4/video_core/renderer_vulkan/fsr411`) |
| `launcher/`, `packaging/` | Лаунчер GTK4; пакет Nix и AppImage |
| `patches/` | Патчи сообщества для Bloodborne |
| `tools/` | Утилиты разработчика: скриптовые запуски, A/B-переключатели, бенчмарк FSR, переписывание шейдеров FSR 4, `fsr4cap` (запись и извлечение FSR 4.1.1) |
| `tests/` | Тесты загрузчика, рантайма, патчей и рендерера |
| `docs/` | Заметки и замеры ([апскейлер](docs/upscaler.md), [многопоточный GPU](docs/parallel_gpu.md), [векторы движения](docs/motion_vectors.md), [план](docs/ROADMAP.md), [журнал разработки](docs/DEVELOPMENT_LOG.ru.md)) |
Тесты: `bash build.sh --test`, `python3 -m unittest discover -s tests` и
`ninja -C out/gpu motion-history-test ui-composition-test scene-resolution-test motion-shader-test settings-test`.
## Что будет дальше
- Новая модель памяти и трансляции по умолчанию — когда она станет стабильной, в том числе на
NVIDIA; после этого старая модель удаляется.
- Дескрипторы ресурсов читает видеочип (bindless), текстуры — объекты, которые создаются при
загрузке: без выполнения кода шейдеров и движка констант на процессоре и без кэша текстур по
адресам.
- Шейдеры переводятся заранее, при установке, а не во время игры.
- Больше параллельности CPU в обработке GPU-команд (дальнейшее разделение стадии записи draw)
с масштабированием на все потоки — важнее всего для Steam Deck.
- Async compute для апскейлера (в 4K кадр упирается в GPU).
- XeSS (суперразрешение) и генерация кадров XeFG через помощник под Wine с общей памятью Vulkan
(прототип моста памяти — `tools/bridge_helper`); DLSS для владельцев NVIDIA; входы апскейлера
в виде, совместимом с подходом OptiScaler.
- Генерация кадров (сначала FSR 3.1 FG), маски реактивности и прозрачности для частиц и тумана.
- Исправление гонок в шейдерах FSR 4.1.1 от AMD при ширине вывода, не кратной 64 (например,
1600×900) — так же, как уже исправлена гонка у левого края в FSR 4 v07 при 1080p.
- Проверка AppImage на Steam Deck; вывод HDR.
## Благодарности и лицензии
bbport распространяется по **GNU GPL v2 или новее** ([LICENSE](LICENSE)) — в нём код shadPS4
(GPL-2.0-or-later). Сторонние компоненты сохраняют свои лицензии:
видеоядро и рекомпилятор шейдеров [shadPS4](https://github.com/shadps4-emu/shadPS4) (GPL-2.0+),
[sirit](https://github.com/shadps4-emu/sirit), [half](https://half.sourceforge.net/),
[FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) от FireBurn (MIT; FSR 3.1 на Vulkan и
провайдер FSR 4 v07), AMD FidelityFX SDK (MIT), [LibAtrac9](https://github.com/Thealexbarney/LibAtrac9)
(MIT), [Dear ImGui](https://github.com/ocornut/imgui) (MIT), шрифты DejaVu,
[dxil-spirv](https://github.com/HansKristian-Work/dxil-spirv) (MIT, для сборки ассетов FSR 4.1.1).
Патчи игры — Kyo, Lance McDonald, auser1337, illusion, emoose и другие участники сообщества
(`patches/Bloodborne.xml`). DLL и данные модели FSR 4 от AMD здесь не распространяются.
+584 -248
View File
@@ -1,281 +1,622 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Version 3, 29 June 2007
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
<https://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
TERMS AND CONDITIONS
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
0. Definitions.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
"This License" refers to version 3 of the GNU General Public License.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
A "covered work" means either the unmodified Program or a work based
on the Program.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
1. Source Code.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
The Corresponding Source for a work in source code form is that
same work.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
2. Basic Permissions.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
13. Use with the GNU Affero General Public License.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
14. Revised Versions of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
NO WARRANTY
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
15. Disclaimer of Warranty.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
@@ -287,15 +628,15 @@ free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
@@ -303,36 +644,31 @@ the "copyright" line and a pointer to where the full notice is found.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Moe Ghoul>, 1 April 1989
Moe Ghoul, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+36
View File
@@ -0,0 +1,36 @@
bbport — a native PC port of Bloodborne (CUSA03173, 1.09)
https://github.com/deadinside28/bloodborne_pc
This copy is the Linux fork, https://github.com/ayozetr/bloodborne_linux. bbport's own files stay
GPL-2.0-or-later, as below. The fork as a whole is distributed under the GNU General Public
License, version 3 or (at your option) any later version (LICENSE has its text): it includes code
under GPL-3.0-or-later (the online module's BBSC and The Hunter's Dream parts), and everything
else in it allows that. Files from other projects keep their own notices.
Copyright 2026 bbport contributors (the authors of this repository's commits and merged pull
requests). Licensed under the GNU General Public License, version 2 or (at your option) any later
version (the fork's LICENSE has version 3's text). No game code or data is part of this project.
Files of this project carry, in their first lines,
SPDX-FileCopyrightText: Copyright 2026 bbport contributors
SPDX-License-Identifier: GPL-2.0-or-later
(or the license and copyright holders of the code they came from). The GPL requires these notices
to be kept intact in copies and in works based on them (GPL-2.0, sections 1 and 2): if you reuse
bbport's code — the runtime in src/, the GPU layer in gpu/, the scripts, the launchers — keep
them, keep this file, and say in your own documentation that the code comes from bbport, with a
link to the repository above. Code reused from a fork of bbport is bbport's code too.
Code bbport is built on, with its own notices kept in its files:
- shadPS4 (https://github.com/shadps4-emu/shadPS4, GPL-2.0-or-later, Copyright shadPS4 Emulator
Project): the GPU layer and the shader translator started from its video core and shader
recompiler and were rewritten part by part (gpu/VENDOR.txt). The online module keeps shadPS4
v0.20.0's network, NP and shadNet code (gpu/bbnet20/vendor/shadps4, gpu/bbnet20/SHADNET-PORT.md).
- The online module's other parts (gpu/bbnet20: BBSC, The Hunter's Dream pool; GPL-3.0-or-later
where their files say so) keep their authors' copyright (gpu/bbnet20/SHADNET-PORT.md).
- The DLSS bridge (gpu/dlss_bridge, MIT), adapted from IFreemz's shadPS4-Bloodborne-DLSS-FSR
(https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR).
- AMD FidelityFX shaders and headers (MIT) in gpu/upscale/shaders/ffx, gpu/third_party and
tools/fsr4cap/ffx, with AMD's notices.
- The libraries in gpu/third_party (sirit, imgui, half, httplib, fsr-vulkan and others), each under
its own license in its folder.
+112 -88
View File
@@ -1,51 +1,104 @@
# Bloodborne for Linux
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own x86-64 code
runs directly on the CPU and its graphics are translated to Vulkan. It is not an emulator.
[![Total Download Count](https://img.shields.io/github/downloads/ayozetr/bloodborne_linux/total.svg?color=2ea44f)](https://github.com/ayozetr/bloodborne_linux/releases/latest)
[![Latest Release](https://img.shields.io/github/v/release/ayozetr/bloodborne_linux.svg?color=8a2be2)](https://github.com/ayozetr/bloodborne_linux/releases/latest)
[![License: GPL-3.0](https://img.shields.io/github/license/ayozetr/bloodborne_linux.svg?color=d73a49)](LICENSE)
[![Platform: Linux | Steam Deck](https://img.shields.io/badge/platform-Linux%20%7C%20Steam%20Deck-blue.svg)](#features)
[![Ko-fi](https://img.shields.io/badge/Ko--fi-support-FF5E5B.svg?logo=ko-fi&logoColor=white)](https://ko-fi.com/ayozetr)
This fork builds on [bbport](https://github.com/deadinside28/bloodborne_pc), the native Linux port
by deadinside28. It adds the features of [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc)
(online play, DLSS, cheats) brought to Linux, plus a few of its own.
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own code runs
directly on the CPU and its graphics are translated to Vulkan: it is not an emulator.
> **No game files are included.** You need your own decrypted dump of Bloodborne, US
> (`CUSA03173`) or EU (`CUSA00900`), game version **1.09**. This project is not affiliated with
> Sony Interactive Entertainment or FromSoftware.
This fork is built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28. It
adds improvements of its own, and others from the community, taken from pull requests and other
projects ([credits](docs/CREDITS.md)).
> **No game files are included.** You need your own decrypted dump of Bloodborne with update
> 1.09, from a retail release ([which ones](docs/COMPATIBILITY.md)). This project is not
> affiliated with Sony Interactive Entertainment or FromSoftware.
**[The players' guide](https://ayozetr.github.io/bloodborne_linux/)**: installing, playing online and
seamless co-op, the Steam Deck, and what to do when something goes wrong.
## Features
- **Online play (beta)** through [The Hunter's Dream](https://thehuntersdream.com) and shadNet:
messages, bloodstains, ghosts, bells and summons. Linux and Windows players can play together.
- **Upscaling:** AMD FSR 3.1, FSR 4 and FSR 4.1.1, **NVIDIA DLSS** on GeForce RTX, and
native-resolution TAA.
- **Unlocked frame rate**, capped at your display's refresh rate up to 120 FPS (above that the
game's own movement timing breaks), or 30/60/90 FPS. Output from 720p to 4K.
- **Launcher** with every setting in one window, in English, Spanish, Italian, French, German,
Portuguese and Russian, like the in-game menu (Insert or L3+R3).
- **Xbox, Switch and keyboard button prompts**, picked by the controller in use, and a gestures
menu without the motion-control icons.
- **Cheats and gameplay tweaks:** never die, stealth, silent steps, enemy control, camera
distance, ragdoll physics and more.
- **Game effects** on and off, **mods** and community **patches**, loaded without changing the
game files.
- **Add to Steam** in one click, and **Discord Rich Presence**.
- **AppImage:** one file with everything inside, for any distribution and the Steam Deck.
- **Multiplayer**: online messages, bloodstains, ghosts, bells and summons, on
[The Hunter's Dream](https://thehuntersdream.com) or a server of your own; **Seamless Co-op**
(experimental), co-op that lasts through deaths, lamps and areas, for up to 5 hunters; and
**local co-op** (split screen), 2 to 4 players on one PC, each with their own controller.
- **In-game map** (Hunter's Atlas): a fullscreen map with where you are, lamps and items, and fast
travel to the lamps you've found.
- **Frame rate unlocked** (up to 120 FPS recommended; the game's movement can break above it)
or capped at **30, 60 or 90**, and changed while you play from the in-game menu. Resolutions from
**720p to 4K**, and any **aspect ratio** (4:3 to 32:9 ultrawide, [how](docs/TROUBLESHOOTING.md#the-picture-is-stretched-on-an-ultrawide-screen)).
- **Upscaling:** AMD **FSR 3.1** and **FSR 4**, NVIDIA **DLSS** (GeForce RTX) and TAA, and
**frame generation** (FSR 3.1, or DLSS on GeForce RTX 40 and newer).
- **Any controller:** PlayStation, Xbox and Switch pads with their own **button prompts in the
game**, and **keyboard and mouse**, with keys you can rebind (bbport 0.7's Dark Souls
III-style layout, or the classic one of earlier Linux versions), movement keys that keep a
sprint through turns (the last of two opposite keys wins; `keyboard_movement=0.7` in
`bbport.ini` for bbport 0.7's ramped stick) and the mouse as a pointer in the game's menus.
- **Exit to desktop** from the game's title and System menus.
- **Cheats and gameplay tweaks:** never die, stealth, camera distance, ragdoll physics and more.
- **Trophies and the platinum**, with a notice in the game like on the PS4.
- **Add to Steam** in one click, and **Discord Rich Presence** with your area, your co-op
partners and a button to this repository.
- **Launcher and in-game menu** (Insert, or hold L3 + R3) in English, Spanish, French, German, Italian,
Portuguese, Russian and Simplified Chinese.
- **AppImage** ready for any distribution and the Steam Deck, which **updates itself** from the
launcher (and can go back to an earlier release).
- **Mods and community patches**, without touching the game files ([how](docs/MODS.en.md)).
## Requirements
- Linux x86-64 and a GPU with Vulkan 1.3: AMD or Intel with Mesa, or NVIDIA with NVIDIA's driver
(open kernel modules included). Mesa's NVK has not been tested.
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's driver; it does not work with NVK.
- About 6 GB of free memory, 10 GB for 1440p or 4K output.
- Linux x86-64 and a GPU with Vulkan 1.3: AMD or Intel with Mesa, or NVIDIA with NVIDIA's driver.
- About 6 GB of free memory, 10 GB for 1440p or 4K.
## Getting started
### AppImage
1. Download `Bloodborne-bbport-x86_64.AppImage` from the
[latest release](https://github.com/ayozetr/bloodborne_linux/releases/latest).
2. Make it executable and run it.
3. Choose your game folder (the one with `eboot.bin`) and press **Play**.
Build it with `bash build.sh && bash packaging/appimage-sharun.sh`, then run
`dist/Bloodborne-bbport-x86_64.AppImage`. Choose your game folder (the one with `eboot.bin`) and
press **Play**. Saves and settings go to `~/.local/share/bbport`.
Saves and settings go to `~/.local/share/bbport`; **Setup → Saves** exports your saves into a
.zip and imports them back, or from shadPS4, and can keep a copy of them from before each game
(off by default). The Game of the Year edition already has The Old
Hunters; with the base game, its DLC goes in **Setup → DLC folder** ([how](docs/COMPATIBILITY.md)).
### From source
## Online play
In the launcher's **Multiplayer → Online** tab, turn on **Play online**, choose a server and press **Test**
in the **Status** row (every line should get a check mark):
- **Seamless Hunt** (the default): the community server of Seamless Co-op (BBSC), with players on
the Windows port. Make an account from the launcher with **Create** ([docs/ONLINE.md](docs/ONLINE.md#seamless-hunt)).
- **The Hunter's Dream**: sign in with Discord. You play with bbhost's players
([docs/ONLINE.md](docs/ONLINE.md#the-hunters-dream-with-bbhosts-players)).
- **Your own server**: for your group, with Docker and Tailscale ([docs/SERVER.md](docs/SERVER.md)).
- **Seamless Co-op (BBSC)**, in the same tab: every player needs it, and summons across areas need
its game server, which your own server can run ([docs/ONLINE.md](docs/ONLINE.md#seamless-co-op-bbsc)).
With online play on, the game starts online by itself, as PLAY ONLINE on a PS4. If you turned off
*Skip the “play online / offline” choice* and pick offline, choose **Log In** on the title screen later
for online play; until then the co-op bells are greyed out.
Everyone in a session needs update 1.09. Turn cheats off when you play with others. Ports,
firewalls and the rest: [docs/ONLINE.md](docs/ONLINE.md).
## Local co-op (experimental)
Several games on one PC, one per player, each in its part of the screen with its own controller
(the launcher's **Multiplayer → Local** tab): choose the players (2 to 4), the layout and each player's
controller, then press **Play together**. Player 1 plays with the launcher's settings and saves;
players 2 to 4 get their own saves and settings in `~/.local/share/bbport/coop/player<N>` (copied
from player 1's the first time, about 400 MB each), without online play or Discord. One player can
use the keyboard and mouse. Only player 1 has sound unless you turn that off. The windows can go
to a monitor of their own (**Monitor**), and **Copy player 1's saves** gives another player the
same characters (their own saves are backed up first). The windows are placed through X11, which
works in a Wayland session too (XWayland).
## Building from source
On Arch Linux (or CachyOS, Manjaro…):
@@ -58,71 +111,42 @@ paru -S magic_enum vulkan-memory-allocator xbyak # from the AUR
git clone --recursive https://github.com/ayozetr/bloodborne_linux
cd bloodborne_linux
bash build.sh
python3 launcher/bbport_launcher.py # or launcher/install-desktop.sh for a menu entry
python3 launcher/bbport_launcher.py
```
With [Nix](https://nixos.org), `bash build.sh` gets every dependency from `shell.nix` by itself.
With [Nix](https://nixos.org), `bash build.sh` gets every dependency by itself. DLSS needs
`bash tools/build_dlss.sh` once; the AppImage is `bash packaging/appimage-sharun.sh`, which bundles
the libraries of the PC it runs on. Releases are built with `bash packaging/appimage-in-arch.sh`
(Docker, plain Arch Linux): on distributions built for newer processors (CachyOS's x86-64-v3
repositories, for example) the AppImage would otherwise not start on CPUs without AVX2. Environment
variables for the launcher's **Advanced → Developer → Extra variables**: [docs/ENVIRONMENT.md](docs/ENVIRONMENT.md).
Problems: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
### Online play
## On Windows
1. Register a shadNet account at [shadnet.shadps4.net](https://shadnet.shadps4.net).
2. In the launcher, open **Online**, turn on **Play online** and enter your account name (not
your email) and password. **Test the connection**: all three servers should be reachable.
3. Start the game and choose online at the title screen.
If you need a Windows version, I recommend
[Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc) and
[Awakenchan's LanternEngine](https://github.com/Awakenchan/LanternEngine).
Everyone in a session needs game version 1.09 and the same servers. Turn cheats off when you
play with others.
## Support
### DLSS
On a PC with a GeForce RTX, run `bash tools/build_dlss.sh` once after `build.sh`. It downloads
what it needs from the NVIDIA DLSS SDK and builds the bridge. Then choose **DLSS** as the
upscaler.
### DLC
The Game of the Year edition (`CUSA03173`) already includes The Old Hunters. With the base game
(such as the EU `CUSA00900`), dump your add-ons too: each one is a folder (`SPEXPANSIONDLC03`,
`SPDLCMESSENGER00`…) with only `sce_sys` inside, because their data ships with the 1.09 update.
Put them in one folder and pick it in the launcher (**Game → DLC folder**), or set
`BB_DLC_DIR=/path/to/dlc` for `run.sh`. The launcher shows how many it found.
### Button prompts
The game shows PlayStation buttons. In the launcher, **Controls → Button prompts** switches them to
Xbox, Nintendo Switch or keyboard ones; *Automatic* follows the controller in use, and the
keyboard's when none is connected. Keyboard prompts show the keys bound in **Controls →
Keyboard**. **Gestures menu without motion-control icons** cleans up the gestures menu. The
prompts are put into your own game's menu files at each start, and nothing in the game folder
changes.
This fork is built on the port deadinside28 created, but it has a lot of hours of work behind it,
and above all hours and hours of testing things and hunting down fixes so it runs as well as it
can. I always try to fix every report as soon as possible, because I know how much this game means
to some people. If it's useful to you and you'd like to support it, you can on
[Ko-fi](https://ko-fi.com/ayozetr).
## Credits
- [deadinside28/bloodborne_pc](https://github.com/deadinside28/bloodborne_pc): **bbport**, the
native Linux port this fork is built on, and almost all of the work behind it. Its README, with
the technical details, is in [BBPORT.md](BBPORT.md).
- [Supermedo/bloodborne_pc](https://github.com/Supermedo/bloodborne_pc): the Windows port, whose
online play, DLSS support, cheats page, Add to Steam and icon were brought to Linux here.
- [shadPS4](https://github.com/shadps4-emu/shadPS4): the renderer and shader recompiler.
- [shadp2p](https://github.com/Wozzardman/shadp2p) by Wozzardman: the network and PSN libraries
for online play. Servers by the [shadPS4](https://shadps4.net) team and
[The Hunter's Dream](https://thehuntersdream.com) by droogie and its team.
- [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR):
the DLSS bridge, and the [NVIDIA DLSS SDK](https://github.com/NVIDIA/DLSS).
- [FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) by FireBurn and the AMD FidelityFX SDK.
- **Dommo**: the Xbox and Switch button prompts (*DS3 Style - Modern Xbox prompts*, *DS3 Style -
Nintendo Switch prompts*) and the *Stripped down gesture menu*, from
[Nexus Mods (Bloodborne, mod 30)](https://www.nexusmods.com/bloodborne/mods/30), uploaded by
goomab. Only their drawn prompts are in this repository ([assets/menu_prompts](assets/menu_prompts)).
- **Abken**: the [KBM Icons Generator](https://www.nexusmods.com/bloodborne/mods/537), which showed
which textures keyboard prompts need (after the idea of
[mod 460](https://www.nexusmods.com/bloodborne/mods/460)); bbport draws its own.
- [quick-sharun](https://github.com/pkgforge-dev/Anylinux-AppImages) for the AppImage.
- Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and the Bloodborne community.
Full list of components and licenses: [BBPORT.md](BBPORT.md#credits-and-licenses).
Built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28, with
[shadPS4](https://github.com/shadps4-emu/shadPS4)'s renderer, and with work from many people and
projects: everyone is in [docs/CREDITS.md](docs/CREDITS.md). It includes code from
Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine). Components and their licenses:
[BBPORT.md](BBPORT.md#credits-and-licenses).
## License
GNU GPL v2 or later ([LICENSE](LICENSE)). Third-party components keep their own licenses. NVIDIA
GNU GPL v3 or later ([LICENSE](LICENSE)). Files taken from other projects keep their own notices:
most are GPL-2.0-or-later (bbport, shadPS4), which allows them to be distributed under
GPL-3.0-or-later. Third-party components keep their own licenses. NVIDIA
and DLSS are trademarks of NVIDIA Corporation.
+1
View File
@@ -0,0 +1 @@
0.7
+5 -3
View File
@@ -11,6 +11,8 @@ Only what the mods drew is here, never the game's own files:
of big prompts from the shared UI atlas (`atlas.dds`, scaled down to the game's atlas size).
- `gesturetop.json`: the edits that turn the game's `menu/gesturetop.gfx` into the mod's.
`scripts/menu_prompts.py` puts them into the player's own `menu/common.tpf.dcx` and
`menu/gesturetop.gfx` at each start; `tools/extract_prompt_assets.py` rebuilds this folder from
the mods' ZIPs. Keyboard prompts are drawn by `scripts/menu_prompts.py` itself.
The port draws its button prompts live (`gpu/shim/bbport_button_prompts.cpp`): `scripts/prompt_art.py`
turns the 20 icons into the raw images it uses for the Xbox and Switch styles, and the keyboard's
are its own. `scripts/gestures_menu.py` applies `gesturetop.json` to the player's own
`menu/gesturetop.gfx` when the launcher's *Gestures menu without motion-control icons* is on.
The Linux fork's `tools/extract_prompt_assets.py` rebuilt this folder from the mods' ZIPs.
+84 -10
View File
@@ -28,25 +28,75 @@ pgo=${BB_PGO:-}
if [[ -z $pgo ]]; then
if [[ -n $(find pgo -name '*.gcda' -print -quit 2>/dev/null) ]]; then pgo=use; else pgo=off; fi
fi
# A profile only applies when its files are named as this build names them (relative to out/gpu,
# -fprofile-prefix-path): one recorded elsewhere matched nothing and PGO was silently off.
if [[ $pgo == use && -z $(find pgo -name 'CMakeFiles#bbgpu.dir#*.gcda' -print -quit 2>/dev/null) ]]; then
echo 'PGO: the profile in pgo/ is from another build layout (rebuild it: BB_PGO=generate); built without it' >&2
pgo=off
fi
mkdir -p pgo
# Submodules (git clone --recursive, or: git submodule update --init) and this port's changes
# to FSR-Vulkan (gpu/patches/fsr-vulkan), applied to its working tree once.
if [[ ! -f gpu/third_party/fsr-vulkan/CMakeLists.txt || ! -f gpu/third_party/imgui/imgui.h ]]; then
git submodule update --init --recursive
fi
for patch in gpu/patches/fsr-vulkan/*.patch; do
if ! git -C gpu/third_party/fsr-vulkan apply --reverse --check "$PWD/$patch" 2>/dev/null; then
git -C gpu/third_party/fsr-vulkan apply "$PWD/$patch"
fi
done
# Issue #124: a checkout updated from an earlier version keeps that version's patch applied, and
# the new one applies neither way ("patch does not apply"): the submodule's tree holds nothing but
# these patches, so it is reset and they are applied again.
fsr_patches_apply() {
for patch in gpu/patches/fsr-vulkan/*.patch; do
if ! git -C gpu/third_party/fsr-vulkan apply --reverse --check "$PWD/$patch" 2>/dev/null; then
git -C gpu/third_party/fsr-vulkan apply "$@" "$PWD/$patch" || return 1
fi
done
}
if ! fsr_patches_apply --check 2>/dev/null; then
echo 'FSR-Vulkan: an earlier version of the port changed it; resetting it and patching again'
git -C gpu/third_party/fsr-vulkan checkout -- .
fi
fsr_patches_apply
cmake -S gpu -B out/gpu -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBB_PGO="$pgo" \
-DBB_LTO="${BB_LTO:-ON}" -DBB_PGO_DIR="$PWD/pgo" >/dev/null
-DBB_LTO="${BB_LTO:-ON}" -DBB_PGO_DIR="$PWD/pgo" > out/gpu-cmake.log
grep '^-- Online module' out/gpu-cmake.log || true
echo "GPU library: PGO $pgo, LTO ${BB_LTO:-ON}"
# A failed GPU build must stop here: an older libbbgpu.so would otherwise be used silently.
if ! ninja -C out/gpu bbgpu > out/gpu-build.log 2>&1; then
if ! ninja -C out/gpu bbgpu $(grep -q "^build bbnet20:" out/gpu/build.ninja && echo bbnet20) > out/gpu-build.log 2>&1; then
grep -v '^\[' out/gpu-build.log | tail -40 >&2
echo 'GPU library build failed (full log: out/gpu-build.log)' >&2; exit 1
fi
# Optional DLSS (NVIDIA RTX): DLSS_SDK_ROOT=<github.com/NVIDIA/DLSS checkout> builds the bridge
# (gpu/dlss_bridge, the only code using NVIDIA's SDK) and puts it next to bb-probe with NVIDIA's
# libnvidia-ngx-dlss.so (Super Resolution) and libnvidia-ngx-dlssg.so (Frame Generation, GeForce
# RTX 40 and newer; SDK 310.7 or newer has it). Without them the DLSS upscaler and DLSS Frame
# Generation are listed as unavailable.
if [[ -n ${DLSS_SDK_ROOT:-} ]]; then
cmake -S gpu/dlss_bridge -B out/dlss-bridge -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DDLSS_SDK_ROOT="$DLSS_SDK_ROOT" >/dev/null
ninja -C out/dlss-bridge >/dev/null
rm -f out/libnvidia-ngx-dlss.so.* out/libnvidia-ngx-dlssg.so.*
cp out/dlss-bridge/libbbport_dlss.so "$DLSS_SDK_ROOT"/lib/Linux_x86_64/rel/libnvidia-ngx-dlss.so.* out/
if ls "$DLSS_SDK_ROOT"/lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.* >/dev/null 2>&1; then
cp "$DLSS_SDK_ROOT"/lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.* out/
echo "DLSS Frame Generation library: $(ls out/libnvidia-ngx-dlssg.so.*)"
else
echo "DLSS Frame Generation library not in this SDK (lib/Linux_x86_64/rel/libnvidia-ngx-dlssg.so.*): DLSS Super Resolution only"
fi
cp "$DLSS_SDK_ROOT/LICENSE.txt" out/NVIDIA-DLSS-LICENSE.txt
echo "DLSS bridge: out/libbbport_dlss.so"
fi
# Installing from PS4 packages (launcher: Install from PKG, scripts/pkg_install.py): pkgextract
# (github.com/paulomanrique/ps4-pkg-extractor, Unlicense) at a reviewed revision, with its
# Cargo.lock. Needs cargo; without it the launcher asks for a game folder only.
pkgextract_rev=f430ce1634fc7953834b545a3946bfbd5fa216fa
if [[ ! -x out/pkgextract/bin/pkgextract ]] && command -v cargo >/dev/null; then
if cargo install --quiet --locked --root out/pkgextract \
--git https://github.com/paulomanrique/ps4-pkg-extractor --rev "$pkgextract_rev" pkgextract \
>out/pkgextract-build.log 2>&1; then
echo 'PKG extractor: out/pkgextract/bin/pkgextract'
else
echo 'pkgextract build failed (out/pkgextract-build.log): Install from PKG is not offered' >&2
fi
fi
# $ORIGIN/gpu: packaged copies keep the library next to the binary without patching it.
gpu=(-Lout/gpu -lbbgpu -Wl,-rpath,'$ORIGIN/gpu' -Wl,-rpath,"$PWD/out/gpu" -rdynamic)
runtime=(src/runtime*.c)
@@ -62,11 +112,15 @@ echo "Built $PWD/out/bb-probe"
# GPU check for run.sh (live_resolution=auto) and the launcher's gamepad list (--gamepads).
"$CC" -std=c11 -O2 -Wall -Wextra -Werror "${includes[@]}" tools/gpu_capabilities.c "${libraries[@]}" -o out/bb-gpu-capabilities
if [[ ${1:-} == --test ]]; then
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -o out/pad-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -lm -o out/pad-test
out/pad-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad_atlas.c "${libraries[@]}" -lm -o out/pad-atlas-test
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 -pthread -Isrc tests/test_file_mods.c -o out/file-mods-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
out/sema-test
@@ -74,5 +128,25 @@ if [[ ${1:-} == --test ]]; then
out/online-test
"$CC" -std=c11 -D_GNU_SOURCE -O2 -g -Wall -Wextra -Werror -I. -Isrc tests/test_content.c src/runtime_content.c -o out/content-test
out/content-test
out/content-test --addons
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_trophy.c src/runtime_trophy.c -o out/trophy-test
out/trophy-test out/trophy_test_tmp
# Hunter's Atlas: its guarded game-memory readers on synthetic images.
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -Isrc tests/test_map_location.c src/runtime_map.c -lm -o out/map-location-test
out/map-location-test
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -Isrc tests/test_atlas_state.c src/runtime_atlas_state.c -lm -o out/atlas-state-test
out/atlas-state-test
# Hunter's Atlas: its tile's state machine, map styles, floor interiors, controller shortcut.
# (-Wno-misleading-indentation: Atlas's compact one-line style.) bbport (Linux fork):
# nlohmann/json from the system, as gpu/CMakeLists.txt.
atlas_cxx=(-std=c++20 -O2 -g -Wall -Wextra -Werror -Wno-misleading-indentation -Igpu/shim -Igpu/third_party/imgui "${includes[@]}")
for test in test_bbport_atlas_menu_state test_bbport_atlas_style test_bbport_atlas_interiors; do
"${CXX:-c++}" "${atlas_cxx[@]}" "tests/$test.cpp" -o "out/$test" || exit 1
(cd out && "./$test") || exit 1
done
"${CXX:-c++}" "${atlas_cxx[@]}" tests/test_map_controller.cpp gpu/shim/bbport_map_controller.cpp "${libraries[@]}" -o out/map-controller-test
out/map-controller-test
"${CXX:-c++}" "${atlas_cxx[@]}" tests/test_atlas_features.cpp gpu/shim/bbport_map_features.cpp "${libraries[@]}" -o out/atlas-features-test
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
fi
-273
View File
@@ -1,273 +0,0 @@
# Изменения в 0.4 (после 0.3, с 2026-10-06)
Замеры — RX 7800 XT / RADV, Yahar'gul стоя, 1080p без лимита FPS; «4 ядра» — `CPUS=0-3,8-11`
(4 ядра / 8 потоков, как у Steam Deck).
## Модель памяти ПК — снова по умолчанию, и для NVIDIA
- `BB_GUEST_IN_PLACE=1` по умолчанию (`run.sh`). В лаунчере вместо «Новой модели памяти» —
переключатель *Старая модель памяти* (ключ `legacy_memory_model`): модель 0.2 как запасной вариант.
- NVIDIA отображает dma-buf только со смещения 0. Раньше порт тогда возвращался к старой модели;
теперь у таких драйверов каждое выделение прямой памяти игры — отдельный кусок памяти Vulkan
(игра делает 14 выделений и каждое отображает один раз целиком, со смещения 0). Проверка при
запуске пишет «the driver's dma-buf maps at offset 0 only: one chunk per direct memory allocation».
`BB_GUEST_WHOLE_CHUNKS=1` включает этот режим и на других драйверах (проверка без NVIDIA):
на RX 7800 XT 188–190 FPS, картинка в порядке. **На NVIDIA не проверено.**
## Уровень API: libGnm игры
Игра собрана со статической libGnm: пакеты PM4 пишет не драйвер, а сама игра, через функции
`sce::Gnm::DrawCommandBuffer` / `DispatchCommandBuffer` / `ConstantCommandBuffer` в
`0x1068000–0x10a0000` и `0x11fd000–0x11fe000`. Это и есть графический API игры — уровень, на
котором работают слои вроде DXVK.
- `tools/re/`: поиск пакетов PM4 в коде (`pm4_scan.py`), граф вызовов по границам функций из
`.eh_frame` (`callgraph.py`), сигнатуры функций по регистрам (`gnm_sigs.py`), таблица перехватов
(`gen_gnm_table.py` → `gpu/shim/bbport_gnm_table.inc`), имена функций (`gnm_names.txt`).
- `gpu/shim/bbport_gnm_hooks.cpp`: перехват 513 функций libGnm в начале (jmp на заглушку рядом с
образом, перенесённый пролог), до запуска кода игры; каждая точка сверяется с eboot 1.09.
- `BB_GNM_CENSUS=1` — счёт вызовов (`BB_GNM_CENSUS_FILE` — полный список в файл).
- `BB_GNM_OBSERVE=1` — каждый метод командного буфера отмечает пакеты, которые записал;
при отправке проверяется, откуда пришёл каждый пакет. Итог: **100,0 % пакетов (99,9 % слов)
пишут известные вызовы API** — командный поток игры целиком описывается вызовами Gnm.
- `BB_GNM_TRACE=адрес[:число]` — аргументы, вызывающий код и записанные пакеты.
- Найдено: игра вызывает 99 методов командных буферов; обратный вызов резервирования
`0x26af230` начинает новый кусок буфера (список кусков уходит в отправку отдельными DCB);
`writeDataInline` (`0x1073b80`) из GX обнуляет 192 байта по разу на проход; метки EOP
(`writeAtEndOfPipe`, значение 1) ставит распределитель памяти GPU, `waitOnAddress` ждёт их
же — это барьер в порядке очереди, а не ожидание CPU.
## Быстрее на слабых процессорах
- `WRITE_DATA` и DMA ждали все отложенные копии хоста (22 мкс на каждый `WRITE_DATA`,
~35 за кадр, 12 % времени потока записи отрисовок на 4 ядрах). Теперь ждут только копии,
которые читают записываемый диапазон (`Scheduler::WaitHostCopiesFor`, таблица источников по
64 КиБ); командный процессор PS4 тоже не ждёт предыдущих отрисовок перед такой записью.
Ожидания: 37 → 2 за кадр. `BB_HOST_COPY_WAITS=all` — прежнее поведение.
- Проверка одновременной записи в планировщик (`ProducerScope`) включается только с
`BB_PRODUCER_CHECK=1` (лаунчер: *Диагностика вылетов*).
- 4 ядра: 158 → 167 FPS.
- `BB_SECTIONS=1` показывает и задачи стадии B по функциям.
## Исправления
- Свёрнутое окно (Win+D, «Показать рабочий стол») больше не завершает игру (issue #31):
поверхность 0×0 не попадает ни в размер кадра, ни в swapchain; кадры, пока окно свёрнуто,
не выводятся. Проверено только, что обычный вывод не изменился: в Hyprland окно не
сворачивается до 0×0.
## Сравнение с 0.3
Лаунчер, раздел «Для разработчика»: *Синхронизация как в 0.3* (`BB_AS_0_3=1` в `run.sh`)
возвращает то, что изменилось после релиза: старая модель памяти, `WRITE_DATA`/DMA ждут все
копии хоста (`BB_HOST_COPY_WAITS=all`), проверка одновременной записи (`BB_PRODUCER_CHECK=1`).
Yahar'gul стоя, 1080p: 123 FPS против ~186 с новой моделью памяти и трансляции. Из командной
строки: `bash tools/play_ab.sh v03 BB_AS_0_3=1`.
## Глубина резкости (DoF) снова работает (issue #44)
С DoF и любым апскейлером (FSR 3/4/4.1.1, TAA) в дальнем размытии — небо, туман — появлялись
тёмные прямоугольники с резкими краями, полосы и перевёрнутая копия сцены; в 0.2 этого не было.
Причина — прицельные ожидания дампов констант из 0.3 (`DumpConstRam` ждёт поток записи отрисовок,
только если пересекается с их чтениями): в список чтений попадали лишь константные буферы, а
вершины и индексы поток записи тоже читает из памяти игры. Игра пишет мелкие данные отрисовок
(в том числе полноэкранные квады DoF) через движок констант, и дамп для следующих отрисовок
перезаписывал их раньше, чем их успевали прочитать. Окно открывалось, когда поток записи
задерживался прямо перед проходами DoF — на проходах апскейлера. Теперь диапазоны вершин и
индексов тоже в списке (неподготовленные отрисовки ждут целиком). Ожиданий стало ~850/с вместо
~150/с, FPS в проверенных местах не изменился. `BB_CONSTRAM_PIPE=0` — полное ожидание на каждом
дампе (A/B).
Возможно, это же чинит мерцающие значки и глифы интерфейса (issue #11) — не проверено.
## Кэш шейдеров больше не ломает запуск (issues #28, #38, #39)
- Файлы кэша пишутся под временным именем и переименовываются: вылет или отключение питания
оставляют старый файл или никакого, а не обрезанный.
- Обрезанная запись при чтении больше не останавливает игру (`Invalid or corrupted
deserialization container`, код 23): повреждённые записи считаются, кэш пересобирается.
То же для SPIR-V с неверным заголовком и для пайплайнов, которые драйвер отверг при
предзагрузке. Код SRT walker из кэша регистрируется только после проверки длины.
## Прочее
- Процессор без AVX, BMI1, MOVBE, LZCNT, POPCNT или SSE4.2 (Intel до Haswell) — понятное
сообщение при запуске вместо `Guest fault (signal 4)`, код 132 (issue #26).
`BB_SKIP_CPU_CHECK=1` — запустить всё равно.
- `tools/fsr4cap/build_assets.sh` молча завершался, если в одной из папок Steam не было
GE-Proton (issue #40).
- Копии памяти игры, поставленные в прямом режиме записи (отрисовка после проходов апскейлера,
`BB_VK_RECORD_THREAD=0`), выполняются сразу, а не со следующей отправкой.
- Внутриигровое меню на английском или русском — переключатель «Language» в меню (PR #36);
по умолчанию язык лаунчера. Лаунчер на бразильском португальском (PR #10); строки без перевода
— на английском. README: адрес репозитория в команде клонирования (PR #43).
- Переключатель `BB_TOGGLE_FILE`: бит 31 отключает сборщик текстур; второе число в файле —
временные биты экспериментов (`BbToggle::Experiment`).
## Векторы движения (ночь на 2026-10-07, проверяется)
- Знак оси Y векторов камеры берётся из вьюпорта G-buffer каждый кадр (`BB_CAMERA_Y=up/down` —
принудительно), а не задан константой: 0.2 и 0.3 ошибались каждая в своих местах
(issues #41, #42, #47, #18).
- Если в кадре несколько проходов G-buffer, камера берётся у того, чья глубина идёт в векторы.
- FSR 4 на пресетах с апскейлом получает кадр в линейном свете (`BB_FSR4_LINEAR=0` — как раньше).
- Маска реактивности работает и с FSR 4: его результат смешивается с текущим кадром на
эффектах (туман, свет, частицы).
## Поток записи отрисовок не ждёт потоки записи команд (2026-10-07)
- Презентер (барьер кадра, FSR 1, постобработка) записывается так же, как отрисовки, на потоках
записи команд. Раньше он брал командный буфер напрямую: поток отрисовок ждал, пока потоки
записи закончат всё переданное (~90 мкс каждый кадр), и сам отправлял кадр (~45 мкс).
- Где команды всё же пишутся напрямую (проходы апскейлера: векторы движения, TAA, FSR 3/4,
резкость), они идут в отдельный сегмент со своим пулом команд, после переданных сегментов,
без ожидания (`BB_DIRECT_SEGMENT=0` — ожидание, как раньше). Сигналы и упорядоченные задачи
в таком сегменте остаются в очереди, чтобы не обогнать более ранние метки.
- Поток показа ждёт, пока отправка кадра (с потока записи) ушла в очередь, прежде чем ставить
своё ожидание на неё: иначе одна очередь ждала бы более позднюю отправку (зависание GPU при
запуске, найдено на этой работе).
- DMA в память, которую GPU использует на месте (модель ПК) или которую писал GPU, — это копия
на GPU в порядке потока; поток отрисовок больше не ждёт перед ней копии и отложенные сигналы
(~16 мкс на каждую, ~900 в секунду: 4-байтовые счётчики GDS).
- Исправлены два давних редких случая записи в уже отправленный командный буфер: смена размера
при резкости больше 1 (`Finish()` посреди прохода апскейлера) и смена пресета FSR 4 в меню.
- `BB_FINAL_DUMP_TRIGGER` (`KIND=final tools/frame.sh`): снимок кадра после FSR и постобработки.
Замеры (Hunter's Dream, стоя, 4 ядра / 8 потоков, модель ПК): 143–144 → 146 FPS; поток
отрисовок больше не блокируется на записи команд (было 1,8–3,3 % времени). 16 потоков: модель ПК
~186, старая модель 144–151 (разброс между запусками ±3 %, без разницы с прежней сборкой).
## Режим работы в лаунчере (2026-10-07)
- Группа *Режим работы*, переключатель *Новая модель памяти и трансляции* (`BB_PC_MODEL=1`),
по умолчанию выключен. Выключен — старая модель памяти, как в 0.3, со всеми исправлениями
после неё. Включён — память игры используется видеочипом на месте, работа командного
процессора переводится в команды Vulkan (экспериментально).
- Только видеокарты AMD: лаунчер находит их по `/sys/class/drm/card*/device/vendor` (0x1002) и
без AMD делает переключатель недоступным; библиотека GPU сама держит режим выключенным, если
выбранное устройство Vulkan не AMD (гибридные ноутбуки). `BB_PC_MODEL_ANY_GPU=1` — включить
всё равно, для проверки.
- Прежний переключатель *Как в версии 0.3* переехал в раздел «Для разработчика» под именем
*Синхронизация как в 0.3*: это откат для поиска регрессий, а не один из двух режимов.
- README (оба) описывают оба режима, таблицу отличий от shadPS4 и планы.
## FSR 4.1.1 из своей DLL — кнопкой в лаунчере (2026-10-07)
- *Апскейлер → FSR 4.1.1 из своей DLL AMD → Выбрать DLL…*: выбираешь
`amd_fidelityfx_upscaler_dx12.dll` 4.1.x, лаунчер собирает ассеты (`tools/fsr4cap/build_assets.sh`)
в фоне, ход — в подписи строки и во вкладке «Журнал», в конце FSR 4.1.1 выбирается. Отмена той
же кнопкой. Без окна: `--build-fsr411 <DLL> [загрузчик]`.
- DLL проверяется до записи (`tools/fsr4cap/dll_info.py`: версия из свойств файла, модели FSR 4
по именам шейдеров). Сборки сообщества OptiScaler (4.0.2b/c: модель `v07_i8`, 16 проходов вместо
29, другие веса) пока не подходят — об этом сразу сообщение, без записи. Загрузчик ищется рядом
с DLL и папкой выше под обоими именами (`amd_fidelityfx_loader_dx12.dll`, у OptiScaler —
`amd_fidelityfx_dx12.dll`), иначе лаунчер его спрашивает.
- Proton запускается через umu-run, если он установлен, иначе в контейнере Steam, который требует
этот Proton (`toolmanifest.vdf`: Steam Linux Runtime 4 для GE-Proton 11) — umu больше не нужен.
- AppImage содержит собранные заранее dxil-spirv и fsr4cap.exe (Nix), SPIRV-Tools: компиляторы и
сеть не нужны, только GE-Proton. Рабочая папка (префикс Proton ~300 МБ) удаляется после сборки.
- Заголовки FidelityFX API (MIT) лежат в `tools/fsr4cap/ffx`: SDK больше не клонируется.
- Новый набор заменяет старый только целиком; коды выхода скрипта (2–7) лаунчер показывает
понятным текстом. Собранная ранее dxil-spirv, которая не запускается (дерево переехало),
собирается заново.
Проверено: из исходников (Nix, umu) и пакет (Steam Linux Runtime 4 в FHS-окружении) — результат
побайтно совпадает с прежним `fsr4_411`; кнопка в лаунчере (headless-сессия): ход «N из 20»,
отказ для 4.0.2c. Не проверено: AppImage на обычном дистрибутиве / Steam Deck (на NixOS AppImage
скрывает системный `/nix` и FHS-ссылки).
- После первой проверки (DLL из Pragmata): это официальная FSR 4.0.3 с той же моделью
`v07_fp8_no_scale`, но её провайдер AMD показывает только на RDNA4 — на RX 7800 XT под любым
Proton DLL предлагает лишь FSR 3.1.5 и 2.3.4. Теперь 4.0.x отклоняется сразу с объяснением.
Отклоняется и «загрузчик», который на самом деле DLL апскейлера (так получилось в видео:
загрузчика рядом не было, и в окне выбора оказалась та же DLL).
- Proton выбирается перебором: первый, под которым DLL включает FSR 4.1 (GE-Proton 10+,
Proton-CachyOS, Proton Experimental, затем Proton 11.0/10.0 из Steam), у каждого свой префикс.
Проверено на RX 7800 XT: GE-Proton 11-6, Proton-CachyOS 11 (июль), Experimental (октябрь) —
да; Proton 11.0-2c и GE-Proton 9 — нет. На Steam Deck был только GE-Proton 9.
- На NixOS AppImage сразу говорит, что рантайм Steam там не запустить (код 8), вместо
непонятного «не запустился под Proton».
## FSR 4.1.1: одна DLL и вариант FP8 для RDNA4 (2026-10-07, день)
- **Загрузчик больше не нужен.** DLL апскейлера AMD сама экспортирует FidelityFX API
(`ffxCreateContext` …), и `fsr4cap.exe` загружает её напрямую; `dll_info.py` проверяет экспорт
по таблице PE. Загрузчик спрашивается только у DLL без этих функций (таких пока не встречалось).
Запись с DLL напрямую совпадает с записью через загрузчик (кроме двух байт неинициализированной
константы в кадре 0), набор — побайтно.
- Повторная сборка из AppImage падала: `cp: cannot create regular file …/fsr4cap.exe: Permission
denied` — копия файла из AppImage только для чтения, и вторая копия поверх неё не создаётся.
Теперь старый файл удаляется перед копированием (и DLL, если она только для чтения).
- **FP8 на RDNA4.** На RDNA4 vkd3d-proton даёт DLL матрицы FP8, и она запускает другой вариант
проходов (issue #12): те же 29 проходов, константы и буферы, но шейдеры на WMMA FP8, свои веса и
своя сетка групп (у каждого прохода свой тайл: 16×8, 32×1, 16×4 …; `_post` — ширина тензора
выровнена до 32, а не до 4). Сборка после INT8 пробует FP8 на первом размере и, если DLL
отдаёт другие шейдеры (RDNA4), записывает все 20 размеров во второй раз (`fp8/capture_*`).
`extract.py` проверяет правила обоих вариантов и пишет `fsr4_411/fp8/` (матрицы FP8,
`VK_EXT_shader_float8`) и `fsr4_411/fp8emu/` (FP8 через FP16-матрицы, как vkd3d-proton на
RDNA3 — только для проверки). Игра выбирает FP8 сама, если у видеокарты есть матрицы FP8 и
набор собран; `BB_FSR411_VARIANT=int8|fp8|fp8emu` — принудительно. В строке
`Upscaler: FSR 4.1.1 replay, …` теперь указан вариант.
- Проходы с матрицами запускаются с подгруппой 32 и полными подгруппами, как у vkd3d-proton
(эвристика размера волны у них совпадает с использованием матриц — `extract.py` это проверяет).
Устройство игры включает `VK_KHR_cooperative_matrix`, `VK_EXT_shader_float8`,
`vulkanMemoryModel`, `computeFullSubgroups`, если они есть.
- Проверено на RX 7800 XT: FP8 с эмуляцией (GE-Proton 11-6, `BB_FSR4CAP_FP8=1`) — DLL и повтор
**побайтно одинаковы** на 7 размерах, обе модели, 4 кадра (`VARIANT=fp8emu verify.sh`; с
dxil-spirv той же версии, что в vkd3d-proton GE-Proton 11-6 — наша закреплённая новее и
иначе округляет преобразования FP8 в эмуляции). INT8 — как прежде (1600×900 расходится у
правого края из-за гонки в самой DLL). На RDNA4 вживую **не проверено**: проходы модели FP8
от обеих версий dxil-spirv одинаковы, различаются только `_post`.
- На RDNA3 FP8 по умолчанию не записывается: эмуляция почти вдвое медленнее INT8 (1440p Balanced,
RX 7800 XT: 3,35 мс против 1,82 мс). `BB_FSR4CAP_FP8=1` —
записать и там, `BB_FSR4CAP_FP8=0` — не записывать нигде.
## Управление и игровое меню (2026-10-07)
- **Меню жестов с тачпада.** Касания тачпада приходили с id 0 и без времени удержания, и игра
их не принимала: ни тачпад, ни Back, ни Tab не открывали жесты. Теперь каждое новое касание
получает свой id (1–127), как у DualShock 4, и время удержания. Проверено в игре: левая половина
тачпада — «Жесты», правая — «Личные вещи» (с включённым debug menu левая половина открывает его).
В лаунчере это подписано у привязок тачпада.
- **Клавиатура работает и при подключённом геймпаде.** Раньше она управляла игрой, только когда
геймпада нет (а на Steam Deck он есть всегда); работали лишь Tab и Backspace. Теперь кнопки
клавиатуры добавляются к кнопкам геймпада, а зажатая клавиша движения/камеры отклоняет стик до
конца. Вживую с клавиатурой не проверено (только сборка и тесты).
- **Меню настроек (Insert / L3+R3) помнит своё место** — и между открытиями, и между запусками
(`menu_pos` в bbport.ini, доля экрана: переживает смену разрешения). Раньше оно каждый раз
открывалось у левого края.
- **Курсор мыши над меню.** Пока меню открыто, показывается системный курсор (в полноэкранном
режиме и после 3 с без движения он скрыт), а меню сразу знает, где мышь, а не после первого
движения.
- **Настройки из меню больше не стирают назначения кнопок.** Меню записывало bbport.ini
целиком, только со своими ключами: назначения клавиатуры и геймпада из лаунчера (`key.*`,
`pad.*`) и другие его ключи пропадали после любого изменения в игре. Теперь меню меняет только
свои строки, остальное оставляет, файл заменяется целиком (обрыв записи оставляет старый).
Тест: `ninja -C out/gpu settings-test`.
## BB_READBACKS=2 больше не зависает при запуске
Точный режим обратного чтения защищает страницы, записанные GPU, и от чтения. Их читали и
потоки копирования порта (выгрузка в VRAM: `bb:Copy*`, `bb:VkRecorder`): чтение падало в
обработчик сбоев, тот ставил обратное чтение в очередь потоку GPU и ждал, а поток GPU в это время
ждал поток записи отрисовок, который ждал эти копии. Теперь копии порта в точном режиме читают
память игры через второе отображение тех же страниц, которое защита не трогает. Проверено: с
`BB_READBACKS=2` игра доходит до титульного экрана и работает (раньше вставала на загрузке).
В самой игре в этом режиме не проверено. `BB_PROBE=<обёртка>` в run.sh — запуск под gdb и т. п.
и из исходников.
## FSR 4.1.1 из AppImage на NixOS (2026-10-07, вечер)
AppImage работает в своём пространстве монтирования с chroot, где её `/nix/store` заменяет
системный: изнутри не видны ни `umu-run`, ни `nix-shell`, ни FHS-окружение Steam, и кнопка
сборки на NixOS заканчивалась ошибкой 8. Теперь `capture_all.sh` на NixOS без `umu-run`:
- если есть `nix-shell` (из исходников, на самой системе) — перезапускается в
`nix-shell -p umu-launcher`;
- в AppImage — копирует себя и `proton.sh` в рабочую папку (`~/.local/share/bbport/fsr4cap`,
её видят обе стороны) и запускает запись на системе через `systemd-run --user --wait --pipe`
в login-shell (PATH и NIX_PATH пользователя); ход и код выхода приходят обратно, отмена
останавливает службу. Перевод в SPIR-V — уже внутри AppImage. `systemd-run` из
systemd-minimal (он и так был в пакете) добавлен в PATH лаунчера.
Проверено на этом ПК (NixOS): кнопка без окна (`--build-fsr411`) из AppImage собирает FSR 4.1.1
до конца, набор побайтно как эталонный, рабочая папка удалена; отмена посреди записи
останавливает службу на системе.
-160
View File
@@ -1,160 +0,0 @@
# Изменения 2026-10-02 (локально, не опубликовано)
Что сделано, как проверить, и что из этого подтверждено замерами. Все замеры — на RX 7800 XT /
RADV; «слабый CPU» — та же машина, ограниченная `taskset` до 4 ядер / 8 потоков.
## Производительность и плавность
### Steam Deck и слабые GPU: путь из релиза 0.1 снова по умолчанию
Для вывода не 1080p (720p на Деке, 1440p, 2160p) вся игра снова рисуется в разрешении пресета
(патч при запуске), как в 0.1. Живое масштабирование оставляло постобработку в 1080p и каждый
кадр копировало цели сцены туда-обратно: на Деке и GTX 1060 это давало 7–8 FPS.
- Выбор пути — настройка «Смена разрешения на лету» (лаунчер, меню, `bbport.ini`
`live_resolution=auto|0|1`, применяется после перезапуска). **Авто** (было по умолчанию; с 03.10 по умолчанию «выключена») включает
живую смену на дискретных GPU с 8+ ГБ памяти, кроме NVIDIA старше Turing: на RX 7800 XT
разрешение и пресеты снова меняются в меню на лету, а Steam Deck и GTX 1060 получают быстрый
путь. Решение пишется в журнал (`GPU: … live resolution changes on/off`); `BB_LIVE_RES=0/1`
перекрывает всё.
- Смена пресета, разрешения вывода или переход на TAA в меню при этом пути — через кнопку
«Применить и перезапустить игру».
- Глубина/stencil уменьшенных целей на GPU без `VK_EXT_shader_stencil_export` (Pascal) снова
в полном размере, без 9-проходного копирования.
### Рваный фреймтайм при упоре в GPU (NVIDIA)
Две причины, обе исправлены:
1. Показ кадра (`vkAcquireNextImage`, `vkQueuePresent`) выполнялся в потоке vblank игры. Когда
GPU загружен, эти вызовы блокируются (на NVIDIA особенно), и vblank-события игры приходили
неравномерно. Теперь показ — в отдельном потоке `bb:Present` (`BB_PRESENT_THREAD=0` — как
раньше).
2. Поток команд GPU убегал вперёд и упирался в случайные ресурсы, флипы шли пачками. Теперь он
опережает GPU не больше чем на `BB_FRAMES_AHEAD` кадров (по умолчанию 1; 0 — без ограничения).
4K без апскейлера, ~78 FPS, медленный поворот камеры (`Frame pacing` в журнале, окна по 5 с):
| | FPS | разброс кадра | p99 | кадров > 1.5× медианы |
|---|---|---|---|---|
| было | 79.5 | 9.9 мс | 25.8 мс | ~125 |
| отдельный поток показа | 79.4 | 5.9 мс | 25.0 мс | ~80 |
| + опережение 1 кадр | 77.4 | **0.9 мс** | **14.6 мс** | **0** |
На слабом CPU (720p FSR 3 Performance, без лимита) опережение на FPS не влияет (166 FPS при 0, 1
и 2), а отдельный поток показа дал +3.5% (160 → 166 FPS).
Новая строка `Frame pacing:` в `BB_FRAME_STATS=1`: медиана, разброс, p99 и число всплесков.
### Кэш шейдеров отключался навсегда (фризы при входе в каждую новую область)
Кэш пайплайнов при запуске сравнивает сохранённый `Shader::Profile` с текущим. В исходном
shadPS4 при несовпадении кэш **закрывается на всю сессию** и ничего не записывает; файл профиля
так и остаётся старым, поэтому дальше кэш не работает никогда, и каждая сессия компилирует все
шейдеры заново. Сегодня профиль изменился (флаг для Pascal), и с ~12:16 кэш у всех запусков был
выключен — включая забег у арены Людвига: в окне фриза 64 компиляции на 114 мс.
Теперь несовместимый кэш очищается и строится заново (одна пересборка, как при обновлении). В
журнале: `Pipeline cache isn't compatible with current system: rebuilding it`. Проверка: забег
по маршруту дважды подряд — 241 компиляция в первом, 28 во втором.
Тот же фриз на выходе с арены (115 мс, забег игрока): кроме компиляций — регистрация 1057
текстур и загрузка 220 МБ изображений и 245 МБ буферов за один кадр (защита страниц 17 мс,
резидентность 10.5 мс, создание изображений 7.9 мс, остальное — копирование). Эту часть ещё
предстоит разнести по кадрам или потокам.
### CPU на слабых процессорах
Профиль (`perf`, 4 ядра / 8 потоков, 720p FSR 3 Performance, без лимита, ходьба): ~166 FPS,
предел — второй этап конвейера (`bb:DrawRec`, занят 88–90%; первый этап ждёт его ~40%).
Время второго этапа размазано по привязке ресурсов и записи draw, единственного горячего места
нет. Для Деки и Haswell (ядра примерно вдвое медленнее) это порядка 85–100 FPS — выше 60, то
есть средний FPS там упирается не в CPU. Главная проблема слабых CPU — фризы подгрузки (см.
«Инструменты»).
- Поток записи Vulkan выбирал время активного ожидания по числу всех потоков машины, а не
доступных процессу: под ограничением ядер он крутился 200 мкс вместо 20.
- Ожидание второго этапа теперь тоже зависит от числа ядер (`BB_PIPE_SPIN_US`); на FPS не
влияет (166.3–166.8 при 0, 50 и 200 мкс), оставлено короче для машин с 8 потоками.
- Ридбэки (Relaxed, защищают лица от «взрыва» вершин): в той же сцене 166.4 FPS против 166.3 с
`BB_READBACKS=0` — ничего не стоят, выключать их незачем.
## Стабильность
### Вылет `Guest fault … 0x263b8e7` с двухэтапным конвейером
`sceGnmSubmitDone` не блокировал игру, если поток команд уже **разобрал** все буферы
(`num_submits == 0`), хотя второй этап конвейера и отложенные метки fence ещё не записали память.
Игра освобождала объекты с метками, поздняя запись портила кучу. Добавлен признак
`work_retired`: GPU считается свободным только после опустошения конвейера и записи меток.
Проверка: 40 минут на 4 ядрах / 8 потоках без падения. Вылет редкий (старая сборка на моей машине
тоже прожила 40 минут), поэтому подтверждением будет только отсутствие вылета у вас.
## Изображение
- **Резкость до 2.0** (лаунчер, меню). До 1 — RCAS самого апскейлера, выше — ещё один проход
RCAS по готовому кадру (FSR 3, FSR 4, FSR 4.1.1, в том числе вывод 720p на Деке; у TAA — свой).
- **Ctrl+клик по ползунку в меню** теперь позволяет ввести число (раньше клавиатура только
стирала: в меню не передавался текстовый ввод SDL).
- **Мерцающие объекты на NVIDIA GTX 10xx (Pascal)** — гробы на мосту и т. п. (shadPS4 PR #1644
чинит это только на Turing и новее через барицентрики). Для GPU NVIDIA без
`VK_KHR_fragment_shader_barycentric` рекомпилятор теперь умножает интерполированное значение
на 1 + 2⁻¹⁶ перед переводом в целое: 0.99999994 снова даёт 1. Нашёл этот приём в 27 шейдерах
Bloodborne; на AMD с принудительным включением (`BB_INTERP_INT_FIX=1`) картинка та же.
**Нужна проверка на GTX 10xx.** Кэш шейдеров один раз пересоберётся у всех (новая версия).
- **Исправления рекомпилятора шейдеров из свежего shadPS4** (вышли после версии, от которой взят
код порта): `V_CVT_PK_U8_F32`, `V_BFM_B32`, `V_ALIGNBIT/ALIGNBYTE_B32` (сдвиг на 32 при нулевом
выравнивании давал неопределённый результат), перенос SCC в `S_ADDC/S_SUBB_U32`, 32-битные
литералы в 64-битных числах, режим плавающей точки compute-шейдеров, селекторы P10/P20 у
`V_INTERP_MOV_F32`. Список — в `gpu/VENDOR.txt`. Такие инструкции встречаются в упаковке
глубины/теней; связь с багами теней (shadPS4 #4384, #3465) не подтверждена — проверьте те места.
- **TAA.** Пробовал: смешивание в тонмапленном пространстве, обрезку истории в YCoCg, обрезку
по дисперсии, восстановление текущего кадра фильтром 3×3. Сравнение в одном запуске с
переключением на лету (неподвижная камера и поворот): ни одна техника не дала выигрыша сверх
разброса между замерами, обрезка YCoCg и фильтр 3×3 — хуже. По умолчанию остался прежний
алгоритм; техники доступны для экспериментов (`BB_TOGGLE_FILE`, биты 51–54). Нужен пример,
где именно TAA нестабилен (место, что видно), чтобы чинить конкретно это.
## Моды, патчи, лаунчер
- **Моды проверены в игре**: мод, подменяющий тексты меню, работает (пути в верхнем регистре,
папка-обёртка). Новое: регистр имён не важен, принимаются `Мод/CUSA03173/dvdroot_ps4`,
папка-обёртка архива и моды без `dvdroot_ps4` (сразу `chr/`, `parts/`…). В журнале:
`Mods: N game files replaced, M added`.
- **Сторонние XML-патчи shadPS4**: папка `patches/` в каталоге данных, группа «Сторонние патчи» в
лаунчере, выбор в `patches.json`. Подробности — [MODS.md](MODS.md).
- **60/90 FPS** больше не падают с `patch overlaps a relocation`: патчи, заменяющие указатели,
загрузчик переносит на реальную базу образа.
- **Лаунчер на английском** (по умолчанию — язык системы; переключатель вверху страницы).
- Подпись «Ограничение FPS»: 0 — по частоте экрана, не выше 120 Гц.
## Память
15 циклов «Выйти из игры → Продолжить» (перезагрузка уровня), 720p FSR 3:
| циклы | RSS процесса | VRAM |
|---|---|---|
| 0 → 7 | +179 МБ | +751 МБ |
| 11 → 15 | +8 МБ | +15 МБ |
Рост затухает: при повторных загрузках это прогрев кэшей (пайплайны, текстуры), а не
неограниченная утечка. `heaptrack` на 8 таких циклах тоже не нашёл источников, растущих от
загрузки к загрузке (крупные — один раз при запуске: таблица страниц 279 МБ, пул текстурного
кэша 92 МБ). Кэш текстур по замыслу освобождает память только у бюджета VRAM. 40-минутный
прогон в одной локации: RSS ~2.64 ГБ, VRAM ~4.29 ГБ всё время, без роста. Не проверено:
долгий проход через много разных локаций (рост кэша до бюджета там ожидаем) — для этого нужен
записанный маршрут.
## Инструменты для тестов
- **Запись маршрута**: `BB_PAD_RECORD=/путь/route.txt` — F9 в игре начинает и останавливает запись
геймпада/клавиатуры. `BB_PAD_REPLAY=/путь/route.txt` + слово `replay` в `BB_PAD_FILE` —
воспроизведение с того же момента. Так можно один раз пробежать маршрут (от лампы у Людвига к
другой лампе), а потом повторять его в замерах фризов. Воспроизведение проверено; запуск записи
клавишей F9 автоматически проверить нельзя — проверьте, пожалуйста.
Как записать: отдохнуть у лампы у Людвига, выйти в меню и «Продолжить» (персонаж появится у
лампы), в лаунчере в «Доп. переменные» указать `BB_PAD_RECORD=/home/<вы>/route.txt`, после
появления нажать F9, пробежать до другой лампы, F9. Сохранение тоже понадобится (папка
`user/savedata`), чтобы старт был в той же точке.
-107
View File
@@ -1,107 +0,0 @@
# Изменения 2026-10-03 (локально, не опубликовано)
Замеры — RX 7800 XT / RADV, ограничение `taskset` до 4 ядер / 8 потоков, настройки Steam Deck
(вывод 1280×720, FSR 3 Performance, сцена 640×360), без лимита FPS. Стенд:
`out/bb-probe` из исходников, сохранение `docs/savedata_route`, скриптовый вход в уровень.
## Смена разрешения на лету — по умолчанию выключена
`live_resolution` без значения (новая установка, лаунчер, меню) теперь означает «выключена»:
для вывода не 1080p — патч разрешения при старте, как в 0.1. `auto` (проверка видеокарты) и `1`
выбираются явно. Тесты `test_run_settings`.
## AppImage без моделей FSR 4.1.1
Модели извлекаются из DLL AMD и в публичный пакет не входят (как в 0.1): `packaging/default.nix`
кладёт `fsr4_411` только с `BB_PACKAGE_FSR411=1`; иначе их ищут в каталоге данных.
## Steam Deck: запуск и сборщик текстур
- **AppImage не запускала игру на NixOS** (код 127 без сообщения): `run.sh` читал
`live_resolution` через `sed`, которого нет в окружении AppImage. Теперь только встроенные
команды bash; тест `test_live_resolution_without_sed_or_grep`.
- **Сборщик текстур на встроенных GPU** (`TextureCache::GarbageCollectImages`). Бюджет считался
как «все кучи минус 8 ГБ для системы» (на Деке ~1 ГБ), а занятость — по кучам GTT, где лежат и
буферы с памятью игры (~4 ГБ). Сборщик постоянно работал в «агрессивном» режиме: на каждой
отправке команд (~66 за кадр) выкидывал до 40 текстур, не использованных 2–3 кадра, и снова
их загружал, а изменённые GPU асинхронно записывал обратно в память игры. Теперь на встроенных
GPU пороги — доля текущего бюджета драйвера (`VK_EXT_memory_budget`): сбор с 70%, нехватка с
85%, критично с 95%. В журнале при нехватке: `Texture cache: memory pressure, …`.
`BB_GC_BUDGET_MB=N` — то же правило с заданным бюджетом на любом GPU (проверка на ПК).
- **Запись вытесненной текстуры обратно в память игры теперь синхронная**, пока страницы ещё
защищены. Отложенная запись приходила после снятия защиты — поверх того, что игра успела туда
записать (например, кучу после выгрузки локации). Возможная причина `Guest fault 0x263b8e7` на
Деке при подгрузке карт; **на ПК этот вылет тоже был один раз за ~10 прогонов, его причина не
найдена** (порог нехватки памяти на ПК ~11 ГБ, сборщик туда не попадает).
## Живая смена разрешения дешевле
Раньше уменьшались только цели 1920×1080; постобработка игры в полуразрешении (960×540, в том
числе цепочка bloom с mip-уровнями) шла в полном размере и каждый кадр пересэмплировала цвет
сцены. На Деке один такой проход стоил ~8.4 мс (против ~1.4 мс при патче разрешения).
- Цели 960×540 уменьшаются тем же коэффициентом, что сцена (ProxySize). Пересчёт FragCoord в
шейдерах не меняется: 960/320 = 1920/640.
- Mip-цепочки полуразрешения: отдельная уменьшенная копия на каждый уровень (ключ — изображение и
уровень); выборка одного уровня читает копию, выборка всей цепочки — родное изображение после
пересэмплирования.
- Переключатель `BB_TOGGLE_FILE` бит 50 (`SceneHalfRes`) — прежнее поведение.
- `BB_SCENE_DEBUG`: ещё печатает пересэмплирования (`Scene resolve/fill`) и чтения уменьшенных
целей в полном размере с причиной (`Scene native read`).
- Тест: `scene-resolution-test` — копия mip-уровня 2 цепочки 960×540 и её возврат в родной уровень.
Результат на ПК (4c/8t): занятость GPU живого пути 3.41 → 3.23 мс на кадр, проход `5a619b89` —
320×180 вместо 960×540. Картинка совпадает с режимом патча (bloom и туман считаются в
пониженном разрешении, как при патче). Осталось: ~9 пересэмплирований и ~7 обратных заливок
цвета сцены 1080p за кадр (копирования, `9a9cf8a9`, композиция UI).
**Почему живой путь всё равно дороже на CPU:** игра считает экран 1080p и рисует в проходе
освещения (`vs 8747a367`) 335 источников света вместо 42 при патче (мелкие источники
отбрасываются по размеру на экране): 975 draw-вызовов на кадр против 679. Это решение самой игры.
## TAA: мерцание тонких объектов вдали
Прутья окон, перила и края обломков мерцали: в фазе jitter, где тонкий объект не попадает в
центр пикселя, проверка глубины отбрасывала историю (в ней объект ближе текущего фона). Теперь
при движении меньше пикселя такая история остаётся и ограничивается цветом соседей (как в TAA без
отбрасывания по глубине). Переключатель бит 55 — прежнее правило.
Метрика: 30 кадров подряд при неподвижной камере (`BB_PRESENT_DUMP_COUNT=30`), средняя вторая
разность яркости по времени (мерцание кадр-к-кадру, плавная анимация почти не влияет), ×1e-3:
| | среднее | p99.9 |
|---|---|---|
| было (2 прогона) | 0.367 / 0.381 | 23.2 / 22.0 |
| стало (2 прогона) | 0.266 / 0.293 | 12.2 / 13.1 |
| без TAA и jitter | 0.428 | 16.8 |
Мягкое ограничение по цвету для неподвижных пикселей и обрезка по дисперсии мерцание не
уменьшили (дисперсия — увеличила). Тесты `taa-shader-test`: добавлены «thin line missed by this
frame keeps history», «static disocclusion is clipped», «moving disocclusion is rejected».
## Апскейлеры: сверка с аудитом от 2026-10-02
Аудит смотрел версию с GitHub (`cc001f9`); по локальному коду:
- Native AA вне 1080p (2.1) — уже исправлено (`w > ow`).
- FSR 4 v07 и HDR-флаг (2.2): провайдер сохраняет флаги контекста, но не читает их — флаг на
результат не влияет. На пути с патчем вход действительно LDR (после тонмаппинга) — ограничение
самого пути.
- Небо (4.1) и порог глубины объектных векторов (4.4) — уже исправлены (поворот без переноса,
относительный порог, полная точность глубины).
- **Mip bias (4.7) — сделано:** в проходах G-buffer (5+ целей) при уменьшенной сцене смещение
LOD сэмплеров log2(рендер / вывод) (на Деке −1); тени, постобработка и UI без изменений. В
журнале `Upscaler: scene texture LOD bias -1.00`. Переключатель бит 57.
- Не сделано: классификация частиц для jitter (4.2), сброс истории на смену камеры (4.8).
## CPU на слабых процессорах: замер
Режим патча, 4c/8t, 209 FPS. Потоки (% одного ядра, пользователь + ядро): `bb:DrawRec` 72 + 14,
поток команд GPU 56 + 20, `bb:VkRecorder` 72 + 1 (в основном время RADV), основной поток игры
46 + 10, четыре потока подготовки draw по ~20. Всего занято ~545% из 800%: предел —
последовательная цепочка «поток команд → запись draw → запись Vulkan». Дальнейший рост требует
распараллелить запись Vulkan (несколько командных буферов) — крупная переделка, не начата.
## Инструменты
- `BB_PRESENT_DUMP_COUNT=N` — `BB_PRESENT_DUMP_TRIGGER` снимает N кадров подряд.
-145
View File
@@ -1,145 +0,0 @@
# Изменения 2026-10-04 — 2026-10-06 (версия 0.3)
Замеры — RX 7800 XT / RADV, Mesa 26, вывод 1080p без лимита FPS, если не сказано иначе.
## Модель памяти ПК — экспериментальная, по умолчанию выключена
Включается переключателем лаунчера «Новая модель памяти (экспериментально)» или
`BB_GUEST_IN_PLACE=1`; по умолчанию работает модель 0.2. Проверена только на RX 7800 XT и Steam
Deck, может вылетать; на NVIDIA dma-buf не отображается со смещением, и порт возвращается к
старой модели. Прямая память игры выделяется через Vulkan
(dma-buf, видимая GPU системная память), и GPU читает её на месте, как буферы игры для ПК. Копий
всей памяти в VRAM и отслеживания записей через защиту страниц больше нет. Метки готовности
(EOP/EOS) пишутся, когда GPU действительно выполнил работу.
- Кадры перекрываются: первые две отправки команд кадра (`BB_SUBMIT_AHEAD=2`) не ждут конца
предыдущего кадра. При K ≥ 8 куча игры течёт (~2 объекта за кадр), поэтому есть сторож: рост
живых выделений на 100 000 выключает перекрытие до конца сессии.
- Отправка команд асинхронная (`BB_ASYNC_SUBMIT`): command buffer'ы сегментов закрывают их потоки
записи, сама отправка — упорядоченная задача на них же.
- Сигналы GPU копятся на стадии записи draw и публикуются пачкой; ожидания ConstRam — точечные.
- Замеры, Yahar'gul, стоя: старая модель 138,7 → 143,6 FPS, новая 101 → 152–155 FPS. Тяжёлое
место (~2000 draw за кадр): 95,5 → 115–124 FPS; там теперь упор в стадию записи draw.
Ключи настроек `pc_memory` и `old_memory_model` больше не читаются: новый ключ `new_memory_model`
по умолчанию выключен у всех.
## Видеопамять больше не растёт от локации к локации
До исправлений VRAM за 13 минут игры вырос с 3,8 до 7,3 ГБ, теперь держится на 4–5,5 ГБ.
- **Сборщик текстур не освобождал ничего.** Десять самых старых текстур, в которые рисовал
GPU, расходовали весь лимит прохода. Теперь такие текстуры пропускаются и уходят в конец
очереди; без нехватки памяти выгружаются текстуры, не использованные `BB_GC_IDLE_SECONDS`
(20) секунд. Возраст считается в секундах: прежние 16 отправок команд (~3 кадра) выкидывали
всё, что ненадолго ушло с экрана.
- **Блоки памяти игры в VRAM никогда не освобождались.** Теперь блоки, не использованные
`BB_VRAM_IDLE_SECONDS` (60) секунд, возвращаются в память игры и снова переносятся в VRAM при
обращении (без этого просадка 90 → 55 FPS: GPU читал их по PCIe). Блоки памяти, которую игра
снимает с отображения, освобождаются. Свободные слоты используются и многоблочными запросами;
новые блоки занимают самые заполненные куски по 64 МБ, почти пустые выселяются, пустые
возвращаются драйверу.
- VMA берёт память блоками по 64 МБ вместо 256 (`BB_VMA_BLOCK_MB`), текстуры от 4 МБ получают
отдельное выделение (`BB_VMA_DEDICATED_MB`): удалённые текстуры не держат большие блоки.
- Статистика кадров: строка `Memory:` — VRAM и GTT игры по данным ядра, RSS, блоки VMA, все
изображения Vulkan, кэш текстур, блоки памяти игры в VRAM, работа сборщика.
`tools/load_watch.sh` пишет VRAM/GTT/RSS каждые 5 с.
## Зависания GPU: найдено место
Метки GPU (`vk_breadcrumbs.*`, `VK_AMD_buffer_marker`; `BB_BREADCRUMBS=0` — выключить): до и
после каждой отрисовки, dispatch и прохода порта GPU пишет номер команды. При потере устройства
журнал называет команду, на которой GPU встал, её шейдеры и параметры.
Найдено (сессия ~20 минут): зависание в непрямом dispatch шейдера частиц `cs 2da7fe60` сразу после
`cs 4ca76892` (1×1×1, генератор случайных чисел эмиттеров, счётчик в GDS). Число итераций циклов
`2da7fe60` берётся из памяти. Аргументы непрямых dispatch теперь копируются GPU в буфер, видимый
процессору: отчёт показывает, что GPU прочитал, а статистика — самые большие непрямые dispatch.
**Причина ещё не устранена**: нужен следующий случай с этими данными.
Зависание больше не выглядит как тихая заморозка картинки при играющем звуке (Steam Deck, бой с
Германом): ожидания GPU замечают потерю устройства, а сторож через 10 с без прогресса GPU пишет
в журнал отчёт меток и останавливает игру с понятной ошибкой.
## Падения и запуск
- Гостевой сбой `0x263b8e7` (частый на Steam Deck): метки EOP писались через `memcpy`, который
записывает 4–7 байт двумя перекрывающимися словами. Игра успевала освободить блок между ними.
Теперь метки пишутся одной атомарной записью (`store_once`, `StoreLabel`). На Deck не проверено.
- NVIDIA с новой моделью памяти падала при запуске: драйвер не отображает dma-buf со смещением.
Перед включением модели порт проверяет это и при неудаче остаётся на старой модели.
- Проверка файлов игры (`scripts/game_check.py`, issue #7, #13, #14): хеш загруженного образа
eboot сравнивается с CUSA03173 1.09; без обновления, со старым eboot или другим изданием запуск
останавливается с объяснением, лаунчер показывает то же. `BB_SKIP_GAME_CHECK=1` — обход.
## Лаунчер и ввод
- Выпадающие списки с длинными вариантами («Фоновая загрузка в видеопамять», конвейер GPU,
чтение данных GPU, контроллер) показывают короткие названия, а пояснение к выбранному —
строкой под заголовком: текст больше не обрезается.
- Выбор контроллера (issue #15): *Управление → Контроллер* (`BB_GAMEPAD`, GUID или часть
имени); выбранный берётся, как только подключится. Список — `bb-gpu-capabilities --gamepads`.
- Ввод имени персонажа в окне поверх игры (issue #19, патч из issue); ввод в игру на это время
нейтрален (Backspace не нажимает тачпад).
- Курсор скрыт в полноэкранном режиме и через 3 с без движения мыши в окне (issue #3).
- **Переназначение клавиатуры и геймпада**: *Управление → Клавиатура / Геймпад*, кнопка
«Назначить» ждёт нажатия. В `bbport.ini` — строки `key.<кнопка>=` и `pad.<кнопка>=` с именами
SDL; нет строки — назначение по умолчанию.
## Патчи игры
- **Sprint Fix (High FPS)**: выше ~147 FPS (а иногда и выше ~90) бег
падал до половины скорости — детектор стены игры мерил расстояние за кадр. Патч включается
всегда вместе с режимами 90 FPS и «без ограничения».
- **Чёрный тонмаппинг на процессорах Intel** (темнее, чем на PS4; сильнее всего в DLC): код игры
выполняется нативно, а приближённые float-инструкции Intel дают другие значения. Патч
исправления включается сам на процессорах Intel; `BB_INTEL_TONEMAP_FIX=0/1` — принудительно.
## Сборка ассетов FSR 4.1.1
- Новейший GE-Proton выбирается по версии (`ls | tail -1` брал 9-x вместо 10-x; issue #4), также
`~/.steam/root` и Flatpak; захват без кадров останавливает сборку с объяснением.
- На RDNA4 DLL, вероятно, выбирает FP8-вариант шейдеров (issue #12): захват скрывает от
vkd3d-proton `VK_KHR_cooperative_matrix`, `VK_NV_cooperative_matrix2`, `VK_EXT_shader_float8`. На
RX 7800 XT захват совпадает побайтно; на RDNA4 не проверено. `extract.py` при нарушении правил
ничего не пишет и объясняет причину.
- Инструменты из системы без Nix; Nix без канала — `channel:nixos-unstable` (issue #22).
## Апскейлеры: мыло и полосы на лестницах
- **Векторы камеры: перевёрнутая вертикаль.** `camera_motion.comp` и проверка глубины в
`taa.comp` считали, что ось +y вида направлена вверх по экрану; у игры она вниз (по строкам).
Ошибка затрагивала наклон камеры и вертикальное движение, поворот влево-вправо был верным.
Поэтому видно было там, где камера покачивается вверх-вниз: на лестнице к собору от гробницы
Идон и в канализации. FSR 4 и 4.1.1 давали полосатое мыло, FSR 3.1 — общее размытие, а персонаж
с векторами объектов оставался чётким. Проверка: на статичных мешах, у которых есть и векторы
объектов (позиции вершин с GPU), новые векторы камеры совпадают с ними в пределах 0,05 px на
17 кадрах бега по лестнице, прежние расходились на 1,3–3,7 px. В игре на FSR 4.1.1 1080p
Performance «стало сильно лучше».
- **Анизотропная фильтрация 16x** для текстур отрисовок сцены (`BB_ANISO`, по умолчанию 16;
0 — фильтры игры). Игра ставит большинству текстур 1x, а при апскейле поверхности под острым
углом теряют детали. Переключатель для A/B на лету — бит 61 в `BB_TOGGLE_FILE`.
- Дамп апскейлера (`BB_DUMP_TRIGGER`) работает и на HDR-пути, пишет матрицы камеры кадра;
`tools/dump_view.py` переводит дамп в PNG.
## MangoHud: двойной оверлей
Текст MangoHud двоился со сдвигом и разными значениями: рисовались два оверлея. Встроенный в
AppImage слой называется `VK_LAYER_MANGOHUD_overlay_64_x86_64` (nixpkgs), системный — иначе,
и загрузчик Vulkan грузил оба. На Steam Deck в игровом режиме то же дают оверлей
производительности Steam (mangoapp) и MangoHud из лаунчера. Теперь `run.sh` при `MANGOHUD=1`
не включает MangoHud в игре, если запущен mangoapp, и отключает встроенный слой, если в системе
есть свой (`VK_LOADER_LAYERS_DISABLE`). На NixOS без системного MangoHud не воспроизводилось.
## Окклюжн куллинг (ветка rainmakerv3/shadPS4 `opts`)
Коммит «correct queries» заменяет фальшивый счётчик ZPASS_DONE настоящими occlusion queries
Vulkan. В Соборном округе Bloodborne делает ~0–8 таких запросов за 5 с и не использует
предикацию (`SET_PREDICATION` — 0), то есть отрисовки по ним не отсекает: переносить нечего.
Сетевой поток в офлайне (коммит про epoll в той же ветке) у нас ядро не занимает.
## Тесты
`tests/test_game_check.py`; `build.sh --test` снова собирается (заглушка
`runtime_memory_note_write` в `tests/test_file_mods.c`).
+53
View File
@@ -0,0 +1,53 @@
# Game compatibility
bbport runs Bloodborne for PS4 with **update 1.09**. Its hooks and patches use the addresses of
one executable: the launcher hashes the loaded `eboot.bin` image and runs only that one 1.09
executable (`scripts/game_check.py`). A dump that passes the check runs; one that does not is
told why (no 1.09 update, another executable, or an `eboot.bin` with Lance McDonald's 60 fps
patch baked in).
## Releases the launcher accepts
Every retail release of Bloodborne. Title and content IDs as listed in
[deadinside28/bloodborne_pc#117](https://github.com/deadinside28/bloodborne_pc/pull/117).
| Edition | Region | Title ID | Content ID | 1.09 executable checked |
|---|---|---|---|---|
| Bloodborne | US | `CUSA00900` | `UP9000-CUSA00900_00-BLOODBORNE000000` | Yes |
| Bloodborne | Europe | `CUSA00207` | `EP9000-CUSA00207_00-BLOODBORNE0000EU` | Not yet |
| Bloodborne | UK | `CUSA00208` | `EP9000-CUSA00208_00-BLOODBORNE0000UK` | Not yet |
| Bloodborne | Japan | `CUSA00299` | `JP9000-CUSA00299_00-BLOODBORNE0000JP` | Not yet |
| Bloodborne | Asia | `CUSA01363` | `HP9000-CUSA01363_00-BLOODBORNE0000AS` | Not yet |
| The Old Hunters Edition | US | `CUSA03179` | `UP9000-CUSA03179_00-BLOODBORNE000000` | Not yet |
| Game of the Year Edition | Europe | `CUSA03173` | `EP9000-CUSA03173_00-BLOODBORNE0000EU` | Yes |
| The Old Hunters Edition | Japan | `CUSA03014` | `JP9000-CUSA03014_00-BLOODBORNE0000JP` | Not yet |
| The Old Hunters Edition | Asia | `CUSA03023` | `HP9000-CUSA03023_00-BLOODBORNE0000AS` | Not yet |
*Checked* means a dump of that release with update 1.09 had exactly the executable bbport runs.
The others are accepted on the expectation that every release shares the same 1.09 executable;
if one does not, the launcher says so instead of starting a game that would crash. A report of
how yours goes is welcome either way.
The alpha tests (`CUSA01318`, `CUSA01322`, `CUSA01333`, `CUSA01335`) are not supported.
The Game of the Year and The Old Hunters editions include the DLC. With a base game (such as the
US `CUSA00900`), dump your add-ons too: each one is a folder (`SPEXPANSIONDLC03`,
`SPDLCMESSENGER00`…) with only `sce_sys` inside, because their data ships with the 1.09 update. Put
them in one folder and choose it in the launcher (**Setup → DLC folder**), or set
`BB_DLC_DIR=/path/to/dlc` for `run.sh`; the launcher shows how many it found. Each folder must keep
the add-on's name (its entitlement label, such as `SPEXPANSIONDLC03`). Copying them to
`user/addcont/<title id>/`, shadPS4's layout, works too.
## Tested with this fork
On Linux (CachyOS, NVIDIA GeForce RTX 3060), with the US release (`CUSA00900`):
- The base game with update 1.09 and the add-ons dumped separately (The Old Hunters and the two
messenger add-ons), from the launcher's DLC folder.
- A dump whose `eboot.bin` had Lance McDonald's 60 fps patch baked in: the launcher reports it,
and the clean 1.09 `eboot.bin` runs.
- Every text and voice language the files have, through *Game text language* and *Voice
language*: Spanish from Spain and Latin American Spanish among them.
The European Game of the Year edition (`CUSA03173`) is the release the original bbport is
developed and tested with.
+109
View File
@@ -0,0 +1,109 @@
# Credits
Everyone whose work is in this fork, and every project it took something from. Everyone listed
under *People* and *Help and testing* also appears in the launcher's **About** section. Components and their licenses: [bbport's notes](../BBPORT.md#credits-and-licenses).
## Projects
| Project | What this fork takes from it |
|---|---|
| [deadinside28/bloodborne_pc](https://github.com/deadinside28/bloodborne_pc) | **bbport**, the native Linux port everything here is built on. Its README is [here](../BBPORT.md). |
| [Supermedo/bloodborne_pc](https://github.com/Supermedo/bloodborne_pc) | The Windows port: online play, the cheats page, Add to Steam, the field of view, the game's audio at full level and many fixes. |
| [Awakenchan/LanternEngine](https://github.com/Awakenchan/LanternEngine) (formerly BloodBorneButPc) | Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine) (GPL-3.0-or-later): crash fixes, stick options, the controller in use, save import, the version selector, and renderer fixes (vertex inputs, index buffers, camera motion vectors). |
| [shadPS4](https://github.com/shadps4-emu/shadPS4) | The renderer and shader recompiler they started from, and the online code: shadPS4 0.20's network, NP and shadNet libraries, put together for bbport by Awaken ([gpu/bbnet20](../gpu/bbnet20)). |
| [Wozzardman/shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) | The shadNet server for a group's own co-op server ([hosting your own server](SERVER.md)). |
| [Classmg22/BBSC](https://github.com/Classmg22/BBSC) | Seamless Co-op, by CLS_AMG, with Wozzardman's seamless summon ([gpu/bbnet20/bbsc](../gpu/bbnet20/bbsc), its game server in [scripts/bbsc_gameserver](../scripts/bbsc_gameserver)). |
| [fhody125-web/NoDreamForHunter](https://github.com/fhody125-web/NoDreamForHunter) | The base of BBSC's game server. |
| [MikeTat1988/HuntersAtlas](https://github.com/MikeTat1988/HuntersAtlas) | Hunter's Atlas: the fullscreen in-game map, its quick-menu tile, lamp travel and the map generator ([tools/hunters-atlas](../tools/hunters-atlas)), which runs with Grimrukh's [soulstruct](https://github.com/Grimrukh/soulstruct). |
| [The Hunter's Dream](https://thehuntersdream.com) | The online server, by droogie and its team; droogie's [bbhost](https://github.com/droogie/bbhost) is the base of its backend in the online module ([gpu/bbnet20/thd](../gpu/bbnet20/thd)) and brought fast loading after a death or travel. |
| [IFreemz/shadPS4-Bloodborne-DLSS-FSR](https://github.com/IFreemz/shadPS4-Bloodborne-DLSS-FSR) | The FSR 4 model without trails. |
| [NVIDIA DLSS SDK](https://github.com/NVIDIA/DLSS) | DLSS. |
| [FireBurn/FSR-Vulkan](https://github.com/FireBurn/FSR-Vulkan) | FSR 3.1, with the AMD FidelityFX SDK. |
| [pkgforge-dev/Anylinux-AppImages](https://github.com/pkgforge-dev/Anylinux-AppImages) | quick-sharun, which builds the AppImage. |
| [paulomanrique/ps4-pkg-extractor](https://github.com/paulomanrique/ps4-pkg-extractor) | The launcher's *Install from PKG*, on [orbis-pkg-util](https://crates.io/crates/orbis-pkg-util). |
| [herumi/xbyak](https://github.com/herumi/xbyak), [fmtlib/fmt](https://github.com/fmtlib/fmt) | Used by the shader translator and the online module. |
| [GoldHEN_Patch_Repository](https://github.com/GoldHEN/GoldHEN_Patch_Repository) | Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and the Bloodborne community: high frame rates, resolution, ultrawide and more. |
| [Nexus Mods: mod 30](https://www.nexusmods.com/bloodborne/mods/30) | Dommo's Xbox and Switch button prompts and stripped-down gestures menu, uploaded by goomab ([assets/menu_prompts](../assets/menu_prompts)). |
## bbport 0.7's contributors
This fork is built on bbport 0.7, which took pull requests and outside work from Qiming-Liu,
AndiMajore, adds39939, iannitram, maiconjsv, whp199, DominickCobb-rs, Sarxzer, wheat32,
ArenaMMORPG, guutol (Luís Gustavo P. Bomfim), redditor69, SrShadowy and Teefpyre, Supermedo and
Awaken, and in earlier releases SrDeTs, IgorWarzocha, itachi-re, Bainianzzz, hideoneric,
guitaripod, unTanana, andrR89, JuanraCM, SaeedShow, msisovic, AyrtonSanabio, axpico, Aryxa,
Obrikash and noelmiller. What each one did:
[bbport's list](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/CONTRIBUTORS_0.7.md).
## People
- **[deadinside28](https://github.com/deadinside28)**: bbport, the port this whole project is built on,
and 0.6's menu pointer and keyboard layout.
- **[Supermedo](https://github.com/Supermedo)**: online play, the cheats page, Add to Steam, and many
fixes from the Windows port.
- **[0xCydral](https://github.com/0xCydral)**: gathered and adapted dozens of fixes (crashes while
loading, black images, the upscaler, the performance line in the log).
- **[Awakenchan](https://github.com/Awakenchan)**: crash fixes (flips, descriptors, page faults),
stick options, the controller in use, save import, the version selector, vertex shaders with
the right bindings, stale vertex inputs read again, index buffers of indirect draws, and camera
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).
- **[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.
- **[FireBurn](https://github.com/FireBurn)**: FSR 3.1.
- **[Jhony-Jhansen](https://github.com/Jhony-Jhansen)**: keyboard movement (the last of two opposite keys wins).
- **[cassioKenji](https://github.com/cassioKenji)**: the keyboard sprint measurements.
- **[msisovic](https://github.com/msisovic)**: why loading hung after a lamp travel (guest thread handles above 40 bits).
- **[axpico](https://github.com/axpico)**: with VRAM full, the game waits for evicted textures to be freed instead of closing.
- **[emurphy04](https://github.com/emurphy04)**: a video player crash when it stops, the field of
view, and an online crash guard.
- **[yumlevi](https://github.com/yumlevi)**: the black character preview, the loading screen's
picture, and a crash on an unreadable shader program.
- **[GoncaloLobo0](https://github.com/GoncaloLobo0)**: crashes while loading areas.
- **[bmy](https://github.com/bmy)**: motion vectors for the large beasts, and a memory fault.
- **[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.
- **[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.
- **[GerasimosKan](https://github.com/GerasimosKan)**: trophy names without the key, and the
sound when one is earned.
- **[artemkaVG](https://github.com/artemkaVG)**: a stencil buffer fix.
- **[parsifal295](https://github.com/parsifal295)**: the touchpad halves on Xbox pads.
- **[AyrtonSanabio](https://github.com/AyrtonSanabio)**: the in-game menu's language without the launcher.
- **[Aryxa](https://github.com/Aryxa)**: DLSS in the AppImage on NVIDIA.
- **[AndiMajore](https://github.com/AndiMajore)**: crash debugging (`BB_FAULT_WAIT`).
- **[iannitram](https://github.com/iannitram)**: FSR 3.1 without colour noise on the Ryzen Z1 (ROG Ally, Legion Go).
- **[whp199](https://github.com/whp199)**: the GX occlusion query guard against the 0x2169edc crash.
- **[nick24051998](https://github.com/nick24051998)**: the SRT walker without faults on null tables.
## Help and testing
- **[willthehuman](https://github.com/willthehuman)**: detailed reports with logs, and testing the fixes.
- **[nr1971](https://github.com/nr1971)**: the game not starting on some graphics cards, the
camera speeding up on diagonals, and a crash on quitting.
- **[Joomzie](https://github.com/Joomzie)**: swapping A and B on Nintendo-style controllers.
- **[Kaka556Kevin](https://github.com/Kaka556Kevin)**: testing the video memory fix.
- **[segasonicye](https://github.com/segasonicye)**: why the game did not start in the Steam Deck's Game Mode.
- **[Monochrome0907](https://github.com/Monochrome0907)**: which effects freeze loading screens on NVIDIA.
- **[bruxis](https://github.com/bruxis)**: the log that showed why Steam Input did not reach the game.
- **[shauryasagar](https://github.com/shauryasagar)** and **[ZeroSTF](https://github.com/ZeroSTF)**: the Hunter's Dream going black after long sessions.
- **[popcar2](https://github.com/popcar2)**: ideas for the launcher.
- **[shaneo88](https://github.com/shaneo88)**: shadPS4's patch file in the patches folder.
- **[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.
- **[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.
- **[OPGZ2](https://github.com/OPGZ2)**: the launcher's blank window at start.
- **[kuday248](https://github.com/kuday248)**: settings from the in-game menu that the launcher overwrote.
- **[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.
+24
View File
@@ -0,0 +1,24 @@
# Catching a crash with a debugger
By default a fatal fault or a GPU-library assertion ends the process at once: the log keeps
one line (`Host fault`, `Guest fault`, `STOP: GPU library assertion failed`) and nothing else.
`BB_FAULT_WAIT=1` keeps the process alive at that point, stopped inside its handler, so a
debugger can look at the faulting thread with its registers, stack and memory still there.
1. Launcher → *Advanced → Developer → Extra variables*: `BB_FAULT_WAIT=1` (or export it before `run.sh`).
Nothing changes while the game runs; it costs nothing.
2. When it crashes, the game freezes, a desktop notification says so, and the log ends with
`bbport: ... the process stays alive for a debugger: bash tools/debug_attach.sh <pid>`.
3. Run that line. gdb attaches, writes `logs/crash-<time>-<pid>.txt` and kills the process.
With `--keep` it detaches instead, so `gdb -p <pid>` can be used by hand afterwards.
The report has every thread's stack, and for the faulting thread its registers, the
instructions around `pc` and the top of its stack; for a GPU-library assertion the locals of
the asserting thread (`bt full`). bbport's own frames carry file and line (the build is
RelWithDebInfo); libc's come from debuginfod when it is reachable. Game code shows as a
*guest offset* (pc minus the image base, as in the log's `Guest fault` line); the report gives
an `objdump` command that disassembles `out/boot-linked.bin` around it.
GPU hangs are different: the device is lost and the GPU breadcrumbs in the log name the stuck
draw or dispatch; the assertion that follows is caught the same way, but the state of interest
is on the GPU.
-188
View File
@@ -1,188 +0,0 @@
# Depth-Adaptive TAA Improvements
**Дата:** 2026-10-01
**Статус:** Исторический эксперимент, заменён исправлением temporal reprojection.
Описанные ниже пороги и веса больше не используются. Уменьшение веса истории вдали
усиливало мерцание; сравнение device depth между разными камерами и хранение глубины
в FP16 не позволяли корректно отличать дальние поверхности. Актуальный разбор,
изменения и результаты проверок: [TEMPORAL_REVIEW_2026-10-01.md](TEMPORAL_REVIEW_2026-10-01.md).
## Проблема
Текущая реализация TAA использует фиксированные пороги для:
1. Определения disocclusion (раскрытие новых поверхностей): `0.0005`
2. Временного веса накопления: `0.9` → `0.65` в зависимости от движения
Эти параметры не учитывают глубину пикселя, что приводит к проблемам:
- **Далёкие объекты (depth → 1.0)**: временная нестабильность, мерцание
- **Близкие объекты (depth → 0.0)**: чрезмерное размытие при быстром движении
## Решение: Адаптивные пороги по глубине
### 1. Depth-Adaptive Disocclusion Threshold
**Файл:** `gpu/shadps4/video_core/host_shaders/taa.comp`
```glsl
float depthThreshold(float depth) {
// Near: 0.001, Far (>0.95): 0.0002
return mix(0.001, 0.0002, smoothstep(0.7, 0.95, depth));
}
```
**Логика:**
- Близкие поверхности (depth < 0.7): порог `0.001` — допускает небольшие расхождения
- Дальние поверхности (depth > 0.95): порог `0.0002` — строже, меньше false positives
- Плавный переход через `smoothstep(0.7, 0.95, depth)`
**Применение:**
```glsl
float depthDiff = abs(history.a - depth);
float threshold = depthThreshold(depth);
if (depthDiff < threshold && ...) {
// Accept history
}
```
### 2. Depth-Adaptive Temporal Weight
```glsl
float temporalWeight(float depth, float motionLength) {
// Base weight decreases for distant objects: 0.9 near, 0.8 far
float baseWeight = mix(0.9, 0.8, smoothstep(0.7, 0.95, depth));
// Motion reduces weight more aggressively for distant pixels
float motionFactor = mix(0.65, 0.5, smoothstep(0.7, 0.95, depth));
return mix(baseWeight, motionFactor, clamp(motionLength / 16.0, 0.0, 1.0));
}
```
**Логика:**
- **Базовый вес без движения:**
- Близко: `0.9` (агрессивное накопление истории)
- Далеко: `0.8` (консервативнее, меньше мерцания)
- **При быстром движении (motionLength > 16px):**
- Близко: снижается до `0.65`
- Далеко: снижается до `0.5` (ещё консервативнее)
**Применение:**
```glsl
float motionLength = length(motion);
float weight = temporalWeight(depth, motionLength);
result = mix(current, clipped, weight);
```
### 3. Motion Vector Validation (camera_motion.comp)
**Файл:** `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
```glsl
// Depth-adaptive validation: distant surfaces need tighter depth matching.
float depth_threshold = mix(0.001, 0.0003, smoothstep(0.7, 0.95, depth));
if (object_motion.b > 0.99 && abs(object_motion.a - depth) < depth_threshold &&
!any(isnan(object_motion.rg)) && !any(isinf(object_motion.rg))) {
result = object_motion.rg;
}
```
**Что изменено:**
- Старый порог: фиксированный `0.001`
- Новый порог: `0.001` → `0.0003` для далёких поверхностей
- Предотвращает использование object motion vectors от неправильных пикселей
### 4. Улучшенная визуализация debug режимов
**Файл:** `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
Режимы `BB_DEBUG_MOTION=1` теперь структурированы:
```glsl
if (mode == 1) {
// Raw depth visualization
imageStore(color_img, pixel, vec4(fract(depth * 4.0), depth, depth < 0.0 ? 1.0 : 0.0, 1.0));
} else if (mode == 2) {
// View-space depth visualization (z / 50)
const float v = z / 50.0;
imageStore(color_img, pixel, vec4(fract(v * 4.0), v, v < 0.0 ? 1.0 : 0.0, 1.0));
}
```
## Ожидаемые улучшения
### Дальние объекты
- ✅ Меньше мерцания на далёких стенах и небе
- ✅ Более стабильная история при небольших движениях камеры
- ✅ Меньше ложных disocclusion от численных ошибок глубины
### Близкие объекты
- ✅ Меньше размытия при быстрых движениях (персонаж, оружие)
- ✅ Более точные object motion vectors
- ✅ Лучший баланс между накоплением и отзывчивостью
### Адаптивная зона (depth 0.7-0.95)
- Плавный переход между режимами
- Предотвращает резкие артефакты на границе
## Тестирование
### Визуальная проверка
```bash
# TAA в нативном разрешении
BB_UPSCALER=taa bash run.sh
# Проверить:
# 1. Мерцание далёких стен (Hunter's Dream, верхние этажи)
# 2. Размытие оружия при быстром повороте
# 3. Стабильность при медленном движении камеры
# 4. Disocclusion при обходе углов
```
### Debug режимы
```bash
# Визуализация глубины
BB_DEBUG_MOTION=1 bash run.sh
# Toggle: (1<<21) = raw depth, (1<<22) = view-space z
# Проверка motion vectors
# Toggle: (1<<20) = blended motion, (1<<23) = reprojection, (1<<24) = difference
```
### A/B сравнение
```bash
# Создать файл для runtime-переключения
echo "0" > /tmp/bb_toggle
# В игре переключать биты для сравнения
# Потребуется временный механизм для старых/новых весов
```
## Потенциальные риски
1. **Слишком консервативные веса на далёких объектах**
- Может увеличить смазывание при быстрых поворотах
- Решение: настроить `baseWeight` (0.8 → 0.85?)
2. **Слишком строгий порог глубины для object motion**
- Может отбрасывать валидные векторы
- Решение: `0.0003` → `0.0005` для depth > 0.95
3. **Smoothstep диапазон 0.7-0.95 может быть неоптимальным**
- Bloodborne: near=0.05, far=3000
- Большинство геометрии в диапазоне 0.1-0.9
- Решение: профилирование распределения глубины в типичных сценах
## Следующие шаги
1. ✅ Реализовать depth-adaptive пороги
2. ⏳ Собрать и протестировать в игре
3. ⏳ Собрать статистику распределения глубины
4. ⏳ Настроить пороги на основе реальных данных
5. ⏳ Добавить runtime-переключение для A/B тестов
6. ⏳ Документировать финальные параметры
## Ссылки
- [upscaler.md](upscaler.md) — общая архитектура апскейлера
- [motion_vectors.md](motion_vectors.md) — векторы движения
- TAA shader: `gpu/shadps4/video_core/host_shaders/taa.comp`
- Camera motion: `gpu/shadps4/video_core/host_shaders/camera_motion.comp`
-447
View File
@@ -1,447 +0,0 @@
# Bloodborne: первый эксперимент нативного запуска
## Частота кадров, звук, диагностика (2026-09-25)
- **FPS**: `BB_FPS=uncap` (по умолчанию) применяет патч «Uncap FPS++»
(Lance McDonald, Kyo) из `patches/Bloodborne.xml`: интервал флипа 1 и
шаги симуляции от реального дельтатайма кадра. Vblank при этом следует за
частотой монитора (`BB_VBLANK_HZ=0`). `BB_FPS=60`/`90` — патчи с
фиксированным шагом, `BB_FPS=30` — без патчей. Дополнительные патчи из XML:
`BB_PATCHES="Skip Intro;Disable Motion Blur (perf increase)"`.
`patches.py` компилирует их в `out/patches.bin`; адреса в XML — это
виртуальные адреса PS4 (база eboot 0x400000), загрузчик пишет их по
смещению `адрес-0x400000` после релокаций (`--patches`).
- **Звук**: `sceAudioOutOutput` теперь возвращает управление строго раз в
период буфера (как железо). Раньше вызовы шли пачками раз в квант PipeWire
(21 мс), поток вывода FMOD обгонял микшер и проигрывал незаписанные половины
512-кадровых блоков — щелчок каждые 10.7 мс.
- **Производительность потока GPU** (`shadPS4:GpuCommandProcessor` — узкое
место: ~97% одного ядра). Замер в одной сцене: 26.5 → 47 FPS.
- `runtime_memory.c`: таблица регионов под rwlock вместо рекурсивного
мьютекса (18% времени поток GPU ждал его), плюс потоковый кеш регионов,
проверяемый по счётчику поколений таблицы, — запросы видеоядра на каждый
draw идут без блокировки.
- `vk_scheduler.cpp`: `PopPendingOperations` на каждом draw делал ioctl
опроса timeline-семафора; теперь только при непустой очереди и не чаще
раза в 250 мкс.
- `fetch_shader.cpp`: разбор fetch-шейдера кешируется по адресу кода с
проверкой копии кода.
- `region_manager.h`: без изменённых страниц в диапазоне защита не
пересчитывается.
- Запись команд Vulkan в отдельном потоке `bb:VkRecorder`
(`Scheduler::Record`, команды и их данные в блоках по 128 КиБ; draw,
dispatch, дескрипторы, барьеры, копирования, dynamic state, детайлинг).
`CommandBuffer()` для непереведённого кода синхронизируется и включает
прямую запись до конца draw. `BB_VK_RECORD_THREAD=0` — старое поведение.
- Кеш описаний текстур (`ImageDesc`) по T# и флагам ресурса, кеш
`FindImage`, проверка изменённых страниц без блокировки для привязок
только на чтение, загрузка гостевых данных буферов в потоке записи.
- Замер в одной сцене: все оптимизации 64 FPS, без них 40 FPS (×1.6).
- `BB_TOGGLE_FILE=<файл>`: маска выключенных оптимизаций, меняется во время
игры (1 кеш регионов, 2 кеш fetch-шейдеров, 4 ранний выход трекинга
страниц, 8 лимит опроса драйвера, 16 поток записи, 32 кеш текстур,
64 проверка страниц без блокировки, 128 кеш FindImage, 256 загрузка
буферов в потоке записи, 512 мемо трекера барьеров) — для поиска
регрессий без перезапуска.
- **Звук**: при старте для Bloodborne выставляется флаг `0x204E` в
`SPRJ0005/userdata0010` (хак rainvmaker из форка Diegolix29): с нулевым
флагом часть звуков, например оружие игрока, не проигрывается.
`BB_SOUND_HACK=0` отключает.
Профилирование без perf: загрузчик по SIGUSR2 печатает RIP, `rdi` (адрес
мьютекса при ожидании futex) и цепочку кадров потока; GPU-библиотека
собирается с `-fno-omit-frame-pointer`.
- **Диагностика**: `BB_FRAME_STATS=1` — FPS, худший кадр и время компиляции
шейдеров/пайплайнов раз в 5 с; `BB_AUDIO_STATS=1` — ритм и заполнение
очереди звука; `BB_AUDIO_DUMP=путь` — сырой PCM каждого порта.
## Текущее состояние (2026-09-25)
Оригинальный x86-64 код `eboot.bin` выполняется напрямую на Linux и проходит
глобальные конструкторы, инициализацию памяти и запуск собственной системы
потоков игры: создано 13 гостевых потоков (`FD4JobWorker:0`, `EzWorkPool_*` ×9,
`MOMainThread`, логгер). Игра выделяет ~4.8 ГиБ direct- и ~337 МиБ
flexible-памяти. Текущая остановка — `sceAjmInitialize` (аудиокодек ATRAC9),
caller `0xd18a6a`.
Изменения этого этапа:
- `runtime_memory.c` переписан: единый sparse memfd на весь direct-пул
(5056 МиБ, как `sceKernelGetDirectMemorySize` на PS4), таблица регионов
с разбиением, flexible memory (448 МиБ), `ReserveVirtualRange`, `Mprotect`,
`QueryMemoryProtection`, `VirtualQuery`, `GetDirectMemoryType`, `BatchMap(2)`,
частичные unmap/release, `MAP_FIXED`/`NO_OVERWRITE`, GPU-биты защиты.
- **Все видимые гостю адреса размещаются ниже 1 ТиБ**: образ, стеки потоков,
главный стек, трамплины (`runtime_low_map`), гостевая память
(`0x10_0000_0000..0xfc_0000_0000`), кучи host-объектов (non-PIE + `mallopt`).
Игра упаковывает указатели в 40 бит: например, lock по адресу `0x20368c0`
хранит владельца как `scePthreadSelf() & 0xffffffffff` и сравнивает с полным
значением. С хэндлами `0x7f…` это давало вечную взаимоблокировку.
- Watchdog (`--timeout`) печатает RIP и цепочку кадров всех потоков
(смещения относительно образа и `bb-probe` для `addr2line`).
`BB_TRACE_SEMA=1` печатает блокирующие ожидания семафоров.
`got_import.py <адрес GOT>` называет импорт за PLT-заглушкой.
- Сеть в режиме «кабель отключён»: HTTP-шаблоны/соединения/запросы создаются,
отправка возвращает сетевую ошибку; NpWebApi/Matching2/Signaling/Score —
«не в PSN»; голосовой чат — порты без звука.
Следующие подсистемы по порядку появления: Ajm (ATRAC9 через ffmpeg),
Fios2, SaveData, Rtc, Pad, AudioOut, AvPlayer, VideoOut и GnmDriver.
## Предыдущее описание
**Оригинальный x86-64 код `eboot.bin` выполняется непосредственно процессором
Linux и проходит начальный участок конструкторов.** Теперь он возвращается из
`_init_env`, регистрирует три обработчика `atexit` и 279 C++-деструкторов,
проходит 1416 guard инициализации, создаёт 1422 mutex, один rwlock и 17 семафоров,
выделяет/отображает 116 МиБ для игрового аллокатора.
Оригинальная `libc.prx` загружается вместе с eboot, её инициализатор возвращает 0,
конструктор `std::ios_base::Init` пройден. Следующая контролируемая остановка —
`sceSysmoduleLoadModule(0xb4)` (`g8cM39EUZ6o#M#N`, caller `0x2026149`).
Отдельный тест Vulkan выполняет команду заполнения буфера и проверяет
возвращённые 4096 байт. shadPS4 в этом эксперименте не запускается.
**Это ещё не рекомпиляция всей игры и не играбельный порт.** Нет окна игры,
меню, отрисовки игровых ресурсов или работающего игрового цикла. Vulkan пока
не соединён с графическими командами игры. Контролируемая остановка на очередном
неподдерживаемом импорте — измеримая граница прототипа; запуск игры не завершён.
## Запуск в текущем окружении
Из `game_files`:
```bash
bash native_probe/run.sh --software
```
Скрипт находит локальные Python, GCC и Vulkan SDK, подготавливает образ,
собирает загрузчик и запускает его. На этой машине инструменты найдены
в `/nix/store`; ничего скачивать или устанавливать не понадобилось.
`--software` выбирает Lavapipe и отключает неявные Vulkan layers для теста.
В проверенном окружении `/dev/dri` отсутствует, поэтому реальный GPU не проверен.
Ожидаемый результат:
```text
Vulkan: llvmpipe (LLVM 21.1.8, 256 bits); command submission + 4096-byte readback PASS
Mapped 91973152 bytes, 4 segments; applied 237083 relocations
Starting native libc.prx at image offset 0x56e0000; fallback exports=202
Runtime: application heap API registered
Native libc module initializer returned 0
Entering original x86-64 code at guest offset 0xa0
Runtime: _init_env returned (verified libc implementation: RET)
Runtime: direct allocation offset=0x0, size=121634816 bytes
Runtime: mapped direct memory, size=121634816, alignment=0x200000
Runtime: semaphore id=1, attr=2, initial=0, max=1
Runtime: semaphore id=2, attr=2, initial=0, max=1
Runtime: semaphore id=3, attr=2, initial=0, max=1
Runtime: semaphore id=4, attr=2, initial=0, max=1
Runtime: semaphore id=5, attr=2, initial=0, max=1
Runtime: semaphore id=6, attr=2, initial=0, max=1
Runtime: semaphore id=7, attr=2, initial=0, max=1
Runtime: semaphore id=8, attr=2, initial=0, max=1
Runtime: semaphore id=9, attr=2, initial=0, max=1
Runtime: semaphore id=10, attr=2, initial=0, max=1
Runtime: semaphore id=11, attr=2, initial=0, max=1
Runtime: semaphore id=12, attr=2, initial=0, max=1
Runtime: semaphore id=13, attr=2, initial=0, max=1
Runtime: semaphore id=14, attr=2, initial=0, max=1
Runtime: semaphore id=15, attr=2, initial=0, max=1
Runtime: semaphore id=16, attr=2, initial=0, max=1
Runtime: semaphore id=17, attr=2, initial=0, max=1
STOP: first unsupported PS4 import: g8cM39EUZ6o#M#N (index 249)
API: sceSysmoduleLoadModule
Caller return offset: 0x2026149; first argument: 0xb4
Runtime: _init_env=1, atexit=3, __cxa_atexit=279, registered handlers=282
Runtime: static guards acquired=1416, released=1416
Runtime: mutexes created=1422, locks=3208, unlocks=3208
Runtime: rwlocks created=1, reads=21, writes=1, unlocks=22
Runtime: semaphores created=17, deleted=0, acquired=0, signals=0, timeouts=0
Runtime: direct memory allocations=1, maps=1, live=121634816, budget=536870912
Runtime: memory operations=9884
Original guest entry instructions executed; game initialization is incomplete.
```
**Код завершения 20 означает ожидаемую остановку на отсутствующей реализации
PS4 API.** Он намеренно отличается от 0. Ошибки загрузки возвращают 1;
явно неподдерживаемые режимы runtime — 21, срабатывание stack protector — 22.
Linux-сигналы — 128 + номер сигнала. На выполнение гостевого кода в Linux
установлен таймер 10 секунд.
На системе с доступным GPU можно попробовать `bash native_probe/run.sh`.
Это использует обычный выбор устройства Vulkan; аппаратный результат здесь
не проверен. Для повторного запуска уже собранных файлов:
```bash
native_probe/out/bb-probe native_probe/out/boot-libc.bin --cpu-only
native_probe/out/bb-probe --vulkan-only
```
Базовый образ без нативной libc сохраняется в `out/boot.bin` для сравнения
с прежней остановкой на `std::ios_base::Init`.
Из каталога `native_probe` команда остаётся прежней: `bash run.sh --software`.
Для воспроизведения самой ранней остановки на `_init_env` используйте
`out/bb-probe out/boot.bin --cpu-only --strict-imports` из этого каталога.
Последняя команда использует обычные настройки Vulkan. Для программного
драйвера задайте `VK_DRIVER_FILES` на его JSON и
`VK_LOADER_LAYERS_DISABLE='~implicit~'`, как делает `run.sh --software`.
## Что найдено в файлах
| Параметр | Результат |
|---|---|
| Игра | Bloodborne, CUSA03173 |
| Версия из `param.sfo` | APP_VER 01.09, VERSION 01.00 |
| Контейнер | SELF, 8 записей сегментов |
| CPU | x86-64, ELF64 little-endian, PS4/FreeBSD ABI |
| Тип ELF | ET_SCE_DYNEXEC (`0xfe10`) |
| Точка входа | `0xa0` относительно базы загрузки |
| Загружаемые сегменты | 2: код/данные только для чтения и изменяемые данные/BSS |
| Образ в памяти | 91 042 036 байт |
| Импорты | 686 уникальных импортированных символов |
| Зависимости DT_NEEDED | 42 модуля |
| Перемещения адресов | 231 478 RELATIVE, 2 838 ABS64, 23 GLOB_DAT, 661 JUMP_SLOT |
| Ресурсы `dvdroot_ps4` | 31 322 968 005 байт |
Среди зависимостей: libc, libkernel, GnmDriver, VideoOut, Pad, Fios2,
AudioOut, SaveData, Sysmodule, сетевые и диалоговые API. В `sce_module`
имеются только семь файлов; наличие файлов модулей не заменяет поддержку их ABI.
Используемые сегменты SELF доступны открытым текстом. Извлечение не выполняет
расшифровку. Неподдерживаемые сжатые/зашифрованные сегменты отклоняются.
Один незагружаемый сегмент метаданных (`program header 9`, тип `0x6fffff01`)
не представлен в доступных SELF-сегментах. В диагностическом `eboot.elf` его
диапазон заполнен нулями: **это не побайтово полное восстановление ELF**.
Все сегменты, нужные этому эксперименту, проверены на наличие.
Подробности, SHA-256 исходного eboot, список зависимостей, NID, сегментов
и количество ресурсов по каталогам находятся в `out/analysis.json`.
Имена нескольких импортов проверены вычислением NID из предполагаемых имён;
результаты записаны в `import_name_hints`. Для первого вызова `_init_env`
получен NID `bzQExy189ZI`, следующий вызов соответствует `atexit`.
## Как устроен прототип
`prepare.py` читает SELF и SFO, проверяет границы, восстанавливает доступные
сегменты, разбирает PS4 dynamic/symbol/RELA-таблицы и формирует `out/boot.bin`.
Исходные файлы открываются только для чтения. Все результаты — в `out/`.
Дополнительно проверяется фактический экспорт `_init_env` в `sce_module/libc.prx`:
NID `bzQExy189ZI`, адрес `0x5fef0`, размер 1, байт `c3` (RET).
SHA-256 libc и доказательство записываются в `analysis.json/libc_evidence`.
Первоначальное предположение о сложной инициализации внутри `_init_env`
для этого дампа не подтвердилось. Возврат соответствует реальному коду функции.
Формат `BBPROBE2` содержит флаг этой проверки; без флага runtime отключён.
Старые образы `BBPROBE1` читаются с отключённым runtime.
`link_libc.py` создаёт отдельный `out/boot-libc.bin` формата `BBPROBE3`:
добавляет два сегмента оригинальной libc, её перемещения адресов и 202
резервные привязки экспортов. Сопоставляет NID вместе с именами и версиями
библиотеки/модуля: локальные суффиксы libc `#C#A` и eboot `#q#q` различаются.
Реализованные host-контракты имеют приоритет. Остальные совпавшие импорты
ссылаются на настоящий код/данные libc, включая объекты и таблицы C++.
Исходные файлы игры не изменяются; сведения о привязках и SHA-256 libc
записываются в `out/libc-link.json`.
Перед входом в eboot выполняется `DT_INIT` libc. Поддержан TLS этой библиотеки:
модуль 2, шаблон 544 байта, блок 1184 байта на host-поток, обнулённый остаток;
14 перемещений `DTPMOD64` записывают номер модуля, а не указатель на образ.
`__tls_get_addr` проверяет модуль и границы. Полный TLS всех модулей PS4,
его освобождение при завершении гостевых потоков и прямой доступ через FS
не реализованы. Для текущего одного гостевого потока блок живёт до выхода.
`sceKernelGetProcParam` возвращает исходный перемещённый procparam eboot;
`_sceKernelRtldSetApplicationHeapAPI` сохраняет таблицу callbacks аллокатора.
`runtime_thread.c` реализует self и запросы init/get/getaffinity/destroy
атрибутов текущего гостевого потока. Для этого прототипа существует один
виртуальный CPU (маска 1); это не маска host CPU и не настройка его affinity.
Создание гостевых потоков и изменение атрибутов остаются неподдержанными.
`probe.c` выделяет память, копирует сегменты и применяет перемещения адресов.
Неизвестные функции направляются в исполняемые перехватчики, печатающие NID
и завершающие процесс. Неподдерживаемые импортированные данные направляются в недоступные
страницы: их использование вызывает ошибку, а не возвращает фиктивные данные.
После загрузки устанавливаются права страниц по ELF, код получает права RX.
Затем выполняется прямой вызов оригинальной точки входа по SysV ABI.
Подготовки аргументов достаточно для наблюдаемого участка; полноценный стек
запуска ядра PS4, исключения и общий механизм загрузки зависимых модулей
не реализованы; пока специально подключена только libc.
`runtime.c` реализует проверенный возврат из `_init_env`, реестр `atexit`/
`__cxa_atexit` с обратным порядком и фильтрацией DSO, `__cxa_finalize`,
`exit`, guard acquire/release/abort, случайный process-local stack canary,
аварийный обработчик повреждения стека, четыре функции работы с памятью и `strlen`.
Сопоставление выполняется по полному NID с library/module suffix именно
этого eboot, без автоматического «вернуть 0» для неизвестных импортов.
Реестр завершения и guards рассчитаны на текущий один гостевой поток;
рекурсивная/конкурентная инициализация guard останавливает выполнение с кодом 21.
`runtime_mutex.c` связывает PS4 mutex/атрибуты с Linux pthread, преобразует
типы и коды ошибок, поддерживает рекурсивные и статически созданные mutex.
Приоритетные протоколы, PS4-планировщик и гостевые потоки не реализованы.
Opaque handles предназначены для реализованных API; двоичный layout
внутренних PS4 mutex не воспроизводится. Наблюдаемый участок libc работает
через эти API; прямой доступ к внутренностям mutex потребует отдельной реализации.
POSIX-импорты mutex из `libScePosix` используют тот же backend через
отдельные wrappers: возвращают положительные PS4 errno, а не отрицательные
коды `scePthread`. Двухаргументный `pthread_mutex_init` не читает отсутствующий
третий аргумент. Linux errno не передаётся гостю напрямую.
`runtime_sema.c` реализует kernel create/delete, wait/poll, signal/cancel.
Handles — уникальные 32-битные ID; удалённые ID не переиспользуются.
Проверены счётчики и переполнение, таймаут в микросекундах с монотонными
часами, резервирование токенов до пробуждения, отмена и удаление с ожидающими
host-потоками. Объект освобождается после возврата всех ожидающих вызовов.
В режиме FIFO обслуживаются подходящие запросы в порядке очереди; запрос
меньшего размера может пройти ожидающий запрос, для которого токенов ещё нет.
Это соответствует использованной справочной модели shadPS4; сопоставление
на реальной PS4 пока не выполнено.
Атрибуты 0/2 допускают создание и неблокирующие операции, но блокирующее
ожидание явно останавливает прототип (21): приоритетный PS4-планировщик ещё
отсутствует. Ненулевые option parameters также не реализованы. В текущем
запуске игра создала 17 семафоров с attr=2, initial=0, max=1, но пока не
выполняла wait/signal; эти операции проверены отдельными C-тестами.
`runtime_time.c` поддерживает POSIX `gettimeofday`: время берётся у host,
поля копируются в явные PS4 timeval/timezone. Проверены границы полей и
диапазон микросекунд. Неожиданная ошибка host API останавливает прототип:
полный POSIX errno/TLS bridge ещё не реализован.
`runtime_rwlock.c` поддерживает init/destroy, read/write, try-read/try-write,
timed-read/timed-write и unlock поверх Linux pthread. Реестр синхронизирует
инициализацию статических handles, отслеживает владельцев, читателей и
ожидающие вызовы: уничтожение занятой блокировки возвращает EBUSY,
чужой unlock — EPERM, таймаут — Orbis ETIMEDOUT. Ожидание не удерживает
mutex реестра. Проверена одновременная работа нескольких host-потоков,
но создание гостевых потоков пока отсутствует. Поддержаны только атрибуты
по умолчанию; нестандартные атрибуты останавливают прототип с кодом 21.
`runtime_memory.c` даёт **ограниченный CPU-пул 512 МиБ для исследования**.
Это выбранный бюджет прототипа, не объём RAM PS4. Выделение возвращает
виртуальный физический offset; Linux memfd обеспечивает реальное общее
хранилище для нескольких отображений и выравнивание 16 КиБ/2 МиБ.
Реализованы allocate/map, полное unmap и полное release. Поддержаны типы
памяти 0/3 для CPU, permissions read/write, flags=0, автоматический адрес;
иной режим останавливается с диагностикой. Частичные release/unmap пока
возвращают EINVAL. GPU coherency, резервирование фиксированных адресов,
полное адресное пространство PS4 и подключение этого пула к Vulkan отсутствуют.
`vulkan_smoke.c` создаёт instance/device/queue, отправляет `vkCmdFillBuffer`,
ставит барьер для чтения CPU, ожидает завершения и сравнивает все слова буфера.
Это проверка будущего графического backend, не переводчик GNM.
## Сборка на других системах
Linux x86-64: Python 3, GCC/Clang, Vulkan headers и loader. Обычно достаточно:
```bash
python3 native_probe/prepare.py CUSA03173
python3 native_probe/link_libc.py CUSA03173
bash native_probe/build.sh
```
Скрипт поддерживает `CC`, `VULKAN_INCLUDE`, `VULKAN_LIB`;
при наличии `pkg-config vulkan` использует его настройки.
Для Windows в исходнике есть ветка `VirtualAlloc`/`VirtualProtect` и явный
SysV ABI. Предполагается **MinGW-w64 GCC/Clang x86-64**, не MSVC.
Эта ветка **не собрана и не проверена** в текущем Linux-окружении.
Пример ручной сборки в окружении MinGW с установленным Vulkan SDK:
```text
python native_probe/prepare.py CUSA03173
gcc -std=c11 -O2 -Wall -Wextra native_probe/probe.c native_probe/runtime.c native_probe/runtime_mutex.c native_probe/runtime_thread.c native_probe/runtime_rwlock.c native_probe/runtime_sema.c native_probe/runtime_time.c native_probe/runtime_memory.c native_probe/vulkan_smoke.c -I<SDK>/Include -L<SDK>/Lib -lvulkan-1 -o native_probe/out/bb-probe.exe
native_probe/out/bb-probe.exe native_probe/out/boot.bin
```
Пути SDK заменяются фактическими; бинарник Linux непереносим на Windows.
Диагностика сигналов и таймер в текущем исходнике реализованы только для Linux.
Новые mutex/rwlock/semaphore/time/direct-memory backends также реализованы только для Linux;
Windows дойдёт до более раннего неподдерживаемого импорта.
## Проверки
```bash
bash native_probe/build.sh --test
python3 -m unittest discover -s native_probe -v
```
Пройдено 35 Python-тестов, включая запуск набора C-проверок runtime:
границы SELF/образа, проверка инструкции `_init_env`, прямое исполнение
x86-64, перемещения адресов, режим strict, отключение неподтверждённого runtime,
диагностика сбоев. C-проверки охватывают обратный порядок деструкторов,
DSO и повторную финализацию, регистрацию при финализации и рост реестра,
guard acquire/abort/release, ошибки и рекурсивность mutex, выравнивание,
непересечение физических выделений, shared aliasing и повторное выделение
очищенной памяти. Stack protector и рекурсивный guard проверены в subprocess.
Дополнительно проверены жизненный цикл rwlock, повторные read-захваты,
чужой unlock, занятое уничтожение, несколько одновременных читателей,
ожидающий writer, преобразование таймаутов и `strlen` с NUL внутри строки.
Добавлены проверки нативного инициализатора/экспорта, приоритета host API,
отключения libc в strict-режиме, прав исполнения и границ новых metadata,
совпадения идентичности при разных локальных ID, отказа при другой версии
библиотеки и обработки TLS-релокаций. C-тест проверяет отдельные TLS-блоки
в host-потоках, шаблон/BSS, запросы атрибутов и регистрацию heap API.
Отдельный `sema-test` проверяет 32-битную запись handle без порчи соседнего
поля, устаревшие ID, переполнение, таймаут, очередь из нескольких host-потоков,
резервирование токенов, отмену/удаление с ожидающими и явный отказ от
приоритетного блокирующего ожидания. Проверены положительные ошибки POSIX
mutex, рекурсивный захват и выходные структуры `gettimeofday`.
Linux-сборка выполнена с `-Wall -Wextra -Werror`.
Актуальный интеграционный запуск настоящего eboot записан в `out/run.log`;
результат старого режима — в `out/strict-run.log`.
## Следующие этапы настоящего порта
1. Реализовать загрузку/реестр системных модулей и необходимые контракты
AppContent. Игра вызвала `sceSysmoduleLoadModule(0xb4)` из `0x2026149`;
по справочной таблице этот ID означает `libSceAppContent`. Такого файла
среди семи локальных `sce_module/*.prx` нет. Потребуется реализация
используемых API и состояния модуля; простой возврат успеха не добавлен.
2. Продолжить конструкторы, дополнить память/синхронизацию, затем реализовать
TLS остальных модулей, запуск потоков и загрузку необходимых модулей.
3. Дойти до `main`, добавить файловую систему (`/app0` → каталог ресурсов)
и фиксировать каждый следующий неподдерживаемый контракт.
4. Перевести GNM/PM4-команды, форматы ресурсов и AMD GCN-шейдеры в Vulkan/SPIR-V;
затем связать VideoOut с окном и swapchain. Наличие Vulkan само по себе
не делает графический код PS4 совместимым.
5. Для статической рекомпиляции восстанавливать функции и контрольный поток,
таблицы виртуальных вызовов, исключения и ABI; проверять поведение сравнением
с исходным машинным кодом. На x86-64 прямое исполнение с совместимым runtime
уже позволяет исследовать запуск без предварительного перевода всего CPU-кода.
Приоритет следующего изменения — Sysmodule/AppContent и следующие конструкторы.
## Справочные источники
Формат SELF и правила соотнесения сегментов сверены с
[заголовками загрузчика shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/loader/elf.h)
и [загрузкой сегментов](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/loader/elf.cpp).
PS4 dynamic-таблицы и вычисление NID сверены с
[module.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/module.cpp),
контекст запуска — с [linker.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/linker.cpp).
Сигнатуры mutex и значения типов сверены с
[mutex.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/mutex.cpp)
и [pthread.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/pthread.h),
rwlock — с [rwlock.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/rwlock.cpp),
сигнатуры direct memory — с
[memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/memory.cpp).
Семафоры сверены с
[semaphore.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/semaphore.cpp),
структуры времени — с
[time.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/time.h).
Идентификатор AppContent — с
[sysmodule_table.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/sysmodule/sysmodule_table.h).
Новые контракты регистрации heap API и procparam сверены с
[linker.h](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/linker.h)
и [process.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp),
атрибуты потоков — с
[pthread_attr.cpp](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/threads/pthread_attr.cpp).
Тело `_init_env` и устройство guards проверены дизассемблированием локальной libc.
Реализация прототипа написана отдельно; зависимости на исполняемый shadPS4 нет.
Каталог `out/` содержит производные данные игры, имеет `.gitignore` и не нужен
для распространения исходников инструмента.
-92
View File
@@ -1,92 +0,0 @@
# Уход от эмуляции: что сделано и что дальше (2026-10-06)
## Где проходит граница API
Bloodborne собран со статической libGnm. Пакеты PM4 пишет не драйвер, а сама игра, функциями
`sce::Gnm::*CommandBuffer` (`0x1068000–0x10a0000`, `0x11fd000–0x11fe000`). Перехват всех таких
функций (`BB_GNM_OBSERVE=1`) показал: 100 % отправляемых пакетов пишут известные вызовы API, всего
99 методов. Gnm — API, который пишет регистры: `setDepthStencilControl` = один `SET_CONTEXT_REG`.
Поэтому разбор PM4 от этих 99 функций и трансляция самих вызовов дают одно и то же; PM4 здесь —
формат записи вызовов, как командный список. Он уже разбирается параллельно (DrawPreparation).
Эмуляция железа — не разбор пакетов, а то, что делается вместо GPU:
| Что | Состояние |
|---|---|
| Модель памяти 0.2 (копии в VRAM, защита страниц, обратное чтение, ранние метки) | Запасной вариант. Модель ПК — по умолчанию, для NVIDIA режим «кусок на выделение». Удалить код 0.2 после проверки на NVIDIA. |
| Метки EOP/EOS | Модель ПК: пишутся после завершения работы GPU (честно). |
| `WaitRegMem` на метки той же очереди | Транслируется как выполненное ожидание (порядок очереди); ждать CPU не нужно. |
| `WRITE_DATA`/DMA ждали все копии хоста | Ждут только пересекающиеся копии (как командный процессор, который не ждёт шейдеров). |
| Constant Engine (CE RAM, дампы, счётчики) | Выполняется на CPU в порядке потока: наш разборщик читает описатели из памяти. |
| SRT walker (код шейдера GCN, исполняемый на CPU) | Нужен по той же причине. |
| Учёт записей GPU для разборщика (`PendingWriteOverlaps`, `IsRegionGpuModified`) | ~15 % потока GpuComm, та же причина. |
| Кэш текстур по адресу + хеш | Та же причина плюс отсутствие объектов «текстура» в PM4. |
Почти вся оставшаяся эмуляция существует потому, что **транслятор читает описатели ресурсов
(T#/V#/S#) из памяти игры на CPU во время разбора**. Драйвер ПК и DXVK так не делают: память
приложения читает GPU во время выполнения.
## Дальше, по порядку
1. **NVIDIA.** Тестовая сборка `dist/Bloodborne-bbport-x86_64-test-1006.AppImage` (модель ПК по
умолчанию, режим «кусок на выделение»). Если работает — удалить модель 0.2 целиком
(`page_manager`, `fault_manager`, обратное чтение, BB_UFFD, предзагрузка, ранние метки).
2. **Распараллелить стадию B** (запись отрисовок; на 4 ядрах занята на 99 %, ~3,3 мкс на
отрисовку, равномерно по привязке буферов/текстур, началу прохода, вершинам). Варианты:
(а) несколько потоков стадии B по диапазонам отрисовок внутри прохода рендеринга (у каждого
свой сегмент команд; общие кэши уже под блокировками; порядок барьеров — по проходам);
(б) разрешение привязок (буферы/текстуры → Vulkan) на стадии A, где 28 % простоя, а B только
записывает. Нужны прогулочные тесты.
3. **Описатели на GPU (главный шаг к «как DXVK»).** Шейдер читает T#/V#/S# сам (буферы на месте
и BDA уже есть); текстуры — bindless-массив, T# → индекс через таблицу на GPU, которую
ведёт кэш текстур при создании образов. Тогда не нужны SRT walker, CE на CPU, учёт ожидающих
записей; `WRITE_DATA`/DMA/дампы CE становятся записями GPU в порядке потока. Большой проект.
4. **Текстуры как объекты.** Перехват `Gnm::Texture::init*` и загрузчика TPF
(`CSGXTextureCreateFragment`): образ создаётся при загрузке, а не угадывается по адресу;
хеши и наложения адресов уходят. Ассеты можно заранее перевести в линейный вид (детайлинг
один раз при установке).
5. **Шейдеры заранее.** Все шейдеры GCN из файлов игры → SPIR-V при установке; без компиляции
во время игры.
Что остаётся трансляцией, как у DXVK: шейдеры GCN → SPIR-V, описатели и форматы → Vulkan.
## Шаг 1 (2026-10-07): движок констант больше не синхронизируется с записью отрисовок
Замеры (`BB_CE_STATS=1`, Яр'гул): ~14 000 `DumpConstRam` в секунду, ~4–5 МБ/с, всего 25 страниц
(кольцо ~100 КБ). Каждый дамп читается ровно одним указателем из user data — это таблицы
описателей для SRT walker. Константы, вершины, индексы, большие буферы из этих страниц не
читаются; шейдеров, читающих память самостоятельно (`uses_dma`), нет. То есть дампы CE нужны
только транслятору — это построение таблиц описателей, а не данные для GPU.
Ожидания стадии A на дампах (`WaitForPendingReads`) были нужны только потому, что стадия B для
отрисовок без подготовки сама перечитывала таблицы V# (описатели вершинных буферов) — к тому
времени перезаписанные следующим дампом (~15 % V# оказывались чужими: баг DoF, issue #44).
Сделано:
- стадия A снимает V# в пакет отрисовки, стадия B берёт их оттуда и таблицы не читает;
- источник DMA отмечается точным диапазоном, а не «читает неизвестно что».
Итог: ожиданий на дампах ~850/с → ~46/с, стадия A ждёт 1,5 % времени вместо ~30 %
(16 потоков). На 4 ядрах / 8 потоках FPS тот же (~170): там узкое место — стадия B
(3,4 мкс на пакет; больше всего — `ObtainBuffer` 0,57 мкс: сопоставление адресов памяти игры с
буферами GPU).
Записи командного процессора (`BB_CE_STATS`): `WRITE_DATA` ~8 400/с в 2 страницы (читаются как
константы ~240/с), DMA ~1 000/с в 15 страниц. Они пишут данные для GPU, и их перевод — команды
GPU в порядке потока (`vkCmdUpdateBuffer`, у DMA для памяти на месте уже так). Для `WRITE_DATA`
это упирается в правило переноса часто записываемых GPU блоков в VRAM — делать вместе с
упрощением модели памяти.
## Шаг 2 (2026-10-07): синхронизации стадии B
Стадия B (запись отрисовок) — узкое место на 4 ядрах (занята 99 %). Задачи кадра на ней:
- сигнал flip = подготовка кадра презентером; 600 тыс. тактов на кадр, из них работы ~40 тыс.,
остальное — ожидание потоков записи команд (`CommandBuffer()`) и синхронная отправка. Теперь
~50 тыс.: презентер записывается через `Record`, отправка асинхронная;
- проходы апскейлера пишут команды напрямую — для них отдельный сегмент без ожидания;
- DMA (~900/с, 4 байта) ждали копии и сигналы даже когда запись делает GPU — ждут только для
записей CPU.
Что осталось на стадии B (мкс на пакет, 3,5 всего): привязка ресурсов 1,0 (буферы 0,4, текстуры
0,45), `ObtainBuffer` 0,6, начало прохода 0,26, вершины 0,34, задачи 0,2. Дальше —
разделение стадии B или привязка ресурсов на стадии A (см. п. 2 выше).
+775
View File
@@ -0,0 +1,775 @@
# Environment variables
The port reads a few hundred `BB_*` environment variables. Some are for players (upscaler, frame
rate, controls); most are diagnostics and developer switches. The list below is every one the code
reads, from the port's own registry (`gpu/shim/bb_env_vars.def`); `tests/test_env_vars.py` fails
when the code and the registry disagree, so it stays complete. Everything from "Every `BB_*`
variable" on is the output of `python3 scripts/env_vars.py --doc` (without its title); the sections
above it are written by hand.
## How to set them
**In the launcher:** *Advanced → Developer → Extra variables (NAME=value, space-separated)*. The
settings key is `extra_env` in the launcher's `settings.json`, read by `game_environment()` in
`launcher/bbport_launcher.py`:
- The field is split on whitespace, and each piece at its **first** `=`:
`BB_UPSCALER=fsr4 BB_AUDIO_GAIN=1.5 RADV_DEBUG=nohiz`.
- **There is no quoting.** A value cannot contain spaces, and quotes stay part of the value. Use
separators the variable itself accepts, such as `;` or `,`.
- A piece without `=` is ignored. If a name appears twice, the last one wins.
- Any name is accepted, third-party ones included (`RADV_DEBUG`, `MANGOHUD`, `VK_*`, `__GL_*`).
- The result goes to `run.sh`. Both the launcher's *Play* button and `--play` (a Steam shortcut,
the Steam Deck's Game Mode) use it.
**From a terminal (source checkout):** `BB_GAME_DIR=/path/to/CUSA03173 BB_FPS=60 bash run.sh`.
Without the launcher, nothing sets the launcher's variables. `run.sh` then uses its own defaults
(game folder `../CUSA03173`, uncapped frame rate, and so on).
**From a terminal (AppImage):** `BB_UPSCALER=fsr4 ./Bloodborne-bbport-x86_64.AppImage --play`
starts the game with the launcher's saved settings. Mind the precedence below: the launcher's own
settings replace a variable of the same name from the shell. Extra variables replace both. The
AppImage keeps its data (bbport.ini, saves, logs) in `BB_DATA_DIR`, by default
`~/.local/share/bbport`.
**bbport.ini:** many graphics options live in `bbport.ini` and the in-game menu, not in the
environment. A few variables override those keys at start: `BB_UPSCALER`, `BB_UPSCALE_PRESET`,
`BB_FSR_SHARPNESS`, `BB_JITTER`, `BB_REACTIVE*` and `BB_OBJECT_MOTION`
(`gpu/shim/bbport_settings.cpp`). Values set this way stay in force until the next start.
## Precedence
The environment is built in this order; a later step wins:
1. The environment the launcher (or the AppImage) was started with.
2. The launcher's own settings (`game_environment()`): some are always written (the game folder,
mods, languages, frame rate, fullscreen and the like) and replace a shell value; others only
when the setting is on.
3. The launcher's **Extra variables**.
4. `run.sh`, which fills in what is still unset (the data folder, the online module, Discord).
Every `BB_*` variable the port reads, by kind, from `gpu/shim/bb_env_vars.def`
(`scripts/env_vars.py --doc`). The game prints the variables set
at its start (`Environment:` in the log) and warns about `BB_*` names it does not know.
`=1: ...` / `=0: ...` / `=N: ...` say what that value does; `set: ...` means any value
turns it on (even `0`).
## 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.
| Variable | What it does |
|---|---|
| `BB_ANISO` | =N: anisotropic filtering of scene textures (default 16) |
| `BB_ASYNC_PIPELINES` | =0: new pipelines of passes drawn every frame compiled at once instead of skipping a draw for a frame or two |
| `BB_ATLAS_ENABLED` | =1 / 0: Hunter's Atlas, the in-game map, on or off (the launcher's Map switch; unset: the map's own settings.ini) |
| `BB_AUTO_RENDER_RES` | =1: render resolution picked from the measured frame rate |
| `BB_BBSC` | =1: seamless co-op (BBSC), the launcher's co-op switch |
| `BB_BBSC_LOCAL_SERVER` | =1 / auto (default) / 0: the BBSC game server on this PC (scripts/bbsc_gameserver) |
| `BB_BUTTON_PROMPTS` | =0: the game's PlayStation button icons always |
| `BB_CJK_FONT` | =path: a font file with Chinese characters for the menu in Simplified Chinese (default: fontconfig's choice for zh-cn) |
| `BB_COMMUNITY_SERVER` | URL of the online pool server (default: the community server) |
| `BB_CONFIG` | =path: the controls file (default bbport.ini) |
| `BB_CONTENT_SKU` | =full / ...: the content profile run.py prepares |
| `BB_CURSOR` | =0: the system's mouse cursor instead of the port's |
| `BB_DECK_DESKTOP_INPUT` | =1: keep keyboard and mouse on a Steam Deck in game mode |
| `BB_DISCORD` | =0: no Discord Rich Presence (the launcher's Discord switch) |
| `BB_DISPLAY` | =N: the monitor to open the window on |
| `BB_DLC_DIR` | =path: the dumped add-ons of this title, one folder each, linked as <user>/addcont/<title id> (the launcher's DLC folder) |
| `BB_DLSS` | =0: no DLSS even on NVIDIA RTX |
| `BB_DMEM_EXTRA_MB` | =N (0-2048, default 512): direct memory beyond the PS4's for the port's extras |
| `BB_DMEM_MB` | =N: the game's direct memory size in MiB (PS4: 5056; raised for high resolutions and large heaps) |
| `BB_EVDEV_TOUCHPAD` | =0: no DualSense/DualShock touchpad through evdev |
| `BB_FFX_DIR` | the folder with AMD's FSR DLLs for Vulkan, amd_fidelityfx_loader_vk.dll and amd_fidelityfx_upscaler_vk.dll (upscaler fsr_amd; default <user>/fsr, then beside the game) |
| `BB_FFX_FG_ML` | =1: AMD's ML frame generation (4.0.x) accepts any GPU (AMD ships it for RDNA4); =0: refuses it (FSR 3.1) |
| `BB_FFX_FG_VERSION` | =<part of a name>: the frame generation AMD's DLL runs, from those the log lists (default: theirs) |
| `BB_FFX_VERSION` | =<part of a name>: the upscaler AMD's FSR DLLs run, from those the log lists (default: theirs) |
| `BB_FPS` | =uncap (default) / 90 / 60 / 30: the frame rate (the launcher's) |
| `BB_FPS_LIMIT` | =N: frame rate limit (the launcher's) |
| `BB_FRAME_GEN` | =fsr3 / dlss / fsr_amd (1: fsr3): frame generation |
| `BB_FRAME_GEN_MULTIPLIER` | =2..4: frames shown per rendered one with frame generation |
| `BB_FRAME_STATS` | set: frame statistics in the log every 5 s and slow frames (the launcher's statistics switch) |
| `BB_FSR411_DIR` | the FSR 4.1.1 assets folder (default fsr4_411) |
| `BB_FSR411_VARIANT` | =int8 / fp8 / fp8emu / navi2 / lite: FSR 4.1.1 model variant (default: the menu's model) |
| `BB_FSR4_DIR` | the FSR 4 v07 assets folder (default fsr4_shaders) |
| `BB_FSR_SHARPNESS` | =0..1: RCAS sharpening (the menu's) |
| `BB_FULLSCREEN` | =0 / 1 / exclusive: window mode |
| `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 |
| `BB_HUD_ANCHOR` | =0: on wide screens the HUD stays in the 16:9 middle |
| `BB_INTEL_TONEMAP_FIX` | =0 / 1: Intel CPU tone mapping fix patch off / on (default on on Intel) |
| `BB_JITTER` | =0 / 1: upscaler camera jitter (the menu's) |
| `BB_JUMP_GAP_MS` | =N (default 70): pause around a keyboard jump while running |
| `BB_JUMP_TAP_MS` | =N (default 70): press length of a keyboard jump while running |
| `BB_KEYBOARD` | =0: local co-op, this game ignores the keyboard and the mouse |
| `BB_KEY_FALL_MS` | =N (default 45): keyboard stick release time |
| `BB_KEY_RISE_MS` | =N (default 70): keyboard stick rise time |
| `BB_KEY_TURN_DPS` | =N (default 1100): keyboard stick turn speed in degrees a second |
| `BB_LANGUAGE` | =N: the game's language, PS4 number (1 English, 8 Russian; the launcher's) |
| `BB_LARGE_HEAPS` | =1 / 0: larger graphics heaps for texture packs on / off (default: detected) |
| `BB_LAYER_MEMORY` | =1 / 0: the layer's memory module (default on non-AMD GPUs) |
| `BB_LOW_LATENCY` | =off / on / boost: NVIDIA Reflex / AMD Anti-Lag (the menu's) |
| `BB_MAP_ART_DIR` | folder: the map's pictures (the launcher's) |
| `BB_MAP_CATALOG` | file: the names of the map's places and items (the launcher's) |
| `BB_MAP_CONFIG` | file: the map's settings (the launcher's) |
| `BB_MAP_DIR` | folder: the maps Hunter's Atlas generated (the launcher's) |
| `BB_MAP_FOG_ENABLED` | =1: areas not visited yet stay hidden on the map |
| `BB_MAP_FONT` | file: the font of the map's names (the launcher's) |
| `BB_MAP_ITEMS_ENABLED` | =1: items on the map (the launcher's) |
| `BB_MAP_ITEM_CATEGORIES` | =list: the items the map shows (healing,insight,equipment,echoes) |
| `BB_MAP_NPC_MODE` | =all / new_dialogue / ...: the NPCs the map shows |
| `BB_MAP_SOUND_GAIN` | =0-2: the level of the map's cursor sounds (0.024, the game's own quick-menu cursor) |
| `BB_MAP_TRAVEL_ENABLED` | =1: travel to lamps from the map (the launcher's) |
| `BB_MENU_LANGUAGE` | =ru / en / ...: the port's menu language (default: the launcher's, else the system's) |
| `BB_MENU_POINTER` | =0: no mouse pointer in the game's menus |
| `BB_MODS_ENABLED` | =0: no mods |
| `BB_MOUSE_AUTO_ROTATE` | =1: the game's camera auto rotation also with the mouse |
| `BB_MOUSE_DEADZONE` | =N (default 16 of 127): mouse look dead zone |
| `BB_NATIVE_MOUSE` | =0: mouse look through the right stick instead of the camera hook |
| `BB_NET_BACKEND` | =thd: The Hunter's Dream online pool (the launcher's online pool) |
| `BB_OBJECT_MOTION` | =0 / 1: motion vectors of moving objects (the menu's) |
| `BB_ONLINE` | =1: online play (the launcher's) |
| `BB_P2P_PORT` | =N: the UDP port for P2P online traffic (default 3658, 0: any) |
| `BB_PAD_BACKGROUND` | =1: local co-op, the controller keeps driving the game and its sound plays while another window has the focus |
| `BB_PAD_DEADZONE` | =N (default 5): stick inner dead zone |
| `BB_PAD_DEADZONE_OUTER` | =N (default 127): stick outer limit |
| `BB_PAD_LIGHTBAR` | =RRGGBB: the light bar colour of DualShock 4 and DualSense pads (the launcher's Controls) |
| `BB_PAD_MOTION` | =0 / 1: motion sensor emulation (over bbport.ini pad_motion) |
| `BB_PATCHES` | =name,...: extra patches by name (patches.py) |
| `BB_PROCESS_PRIORITY` | =normal / above / high: process priority on Windows (default above) |
| `BB_REACTIVE` | =1: FSR reactive mask (the menu's) |
| `BB_REACTIVE_MAX` | reactive mask maximum (the menu's) |
| `BB_REACTIVE_SCALE` | reactive mask scale (the menu's) |
| `BB_REACTIVE_THRESHOLD` | reactive mask threshold (the menu's) |
| `BB_RENDER_RES` | =WxH: render resolution through the startup patch (compatibility) |
| `BB_SAVE_LOG` | =1: the log and frame statistics also kept in logs/ |
| `BB_SEAMLESSHUNT_TOMBS` | =1: keep Seamless Hunt's bloodstains and death visions (they crash the game; scripts/online.py) |
| `BB_SHADER_PROGRESS` | =0: no shader compilation progress on screen |
| `BB_SHADNET_NPID` | the shadNet account name (the launcher's) |
| `BB_SHADNET_PASSWORD` | the shadNet account password (the launcher's) |
| `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 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 |
| `BB_THD_ONLINE_ID` | The Hunter's Dream online name (the launcher's) |
| `BB_THD_TOKEN` | The Hunter's Dream account token (the launcher's) |
| `BB_TROPHY_KEY` | the trophy data key (from the environment or the port's .env) |
| `BB_UPNP` | =1: UPnP port forwarding for online play |
| `BB_UPSCALER` | =none / fsr3 / fsr4 / dlss / ...: the upscaler over the menu's (scripts, A/B) |
| `BB_UPSCALE_PRESET` | the upscaler quality preset (the menu's) |
| `BB_USER_NAME` | the PS4 user name (default Hunter) |
| `BB_WINDOW_RECT` | =x,y,w,h: local co-op, the window's place and size as fractions of its display, without borders |
## Tuning (235)
Defaults to change on a machine where they misbehave, workarounds, and switches that turn a fix off for comparison.
| Variable | What it does |
|---|---|
| `BB_ADB` | path of adb for BB_APP0_ADB (default: adb) |
| `BB_ANTI_LAG` | =1: AMD Anti-Lag 2 through Mesa's layer (measured ~0 gain, #289) |
| `BB_APP0_ADB` | =serial: the game's files fetched from a phone over adb on first open |
| `BB_ASYNC_PIPELINE_THREADS` | =N: threads compiling pipelines in the background |
| `BB_ASYNC_SUBMIT` | =0: the presenter submits the frame itself instead of a submit thread |
| `BB_AUDIO` | =none: no audio device (headless runs) |
| `BB_AUDIO_GAIN` | =N (0-8, default 1): the game's sound level on top of the fold to the device's channels |
| `BB_AUTO_NOHIZ` | =0 (run.sh): no automatic RADV_DEBUG=nohiz on the GPUs that need it |
| `BB_AVPLAYER_ASYNC_READY` | =1: movie StateReady on the controller thread, as before (crashed at 200+ FPS) |
| `BB_BDA_DEDICATED_MB` | =N: buffers with GPU addresses get dedicated memory from N MiB |
| `BB_BIND_MEMO` | =0: every resource binding takes the full path (no memo) |
| `BB_BREADCRUMBS` | =0: no GPU breadcrumbs (what the GPU was drawing at a hang) |
| `BB_CHUNK_TEXTURES` | =0: texture data copied instead of read in place from guest memory chunks |
| `BB_CHUNK_UPLOADS` | =0: uploads through CPU staging instead of binding guest memory in place |
| `BB_COMMIT_IMPORT_LOCKED` | =1: guest memory commits import under the lock |
| `BB_COMPUTE_RELEASE_FLUSH` | =0: work before a compute release waits for the rest of the frame |
| `BB_CONSTANTS_IN_PLACE` | =1: shader constants bound in place from guest memory instead of copied |
| `BB_CONSTANT_RING_MB` | =N: size of the constant ring in MiB |
| `BB_CONSTRAM_GPU_WAIT` | =0: a constant RAM dump does not wait for queued draws reading it (DoF artifacts #44) |
| `BB_CONSTRAM_PIPE` | =0: a constant RAM dump waits for all queued draws |
| `BB_COPY_DEST_GAP` | =bytes: merge gap of copy shader destinations (default 256) |
| `BB_COPY_DEST_PRECISE` | =0: the whole bound destination of a copy shader counts as written |
| `BB_COPY_FILTER_GATE` | =1 with BB_COPY_SKIP_SAME: the copy filter off while the draw recording thread is over 92% busy |
| `BB_COPY_GPU_BUFFERS` | =0: command buffers decoded from guest memory instead of a copy per submission |
| `BB_COPY_MERGE_KB` | =N: copy shader ranges closer than N KiB merged (default 64) |
| `BB_COPY_PAGED_RANGES` | =0: copy shader ranges whole instead of by page |
| `BB_COPY_SHADER_NATIVE` | =0: the copy shader's list read on the CPU |
| `BB_COPY_SKIP_SAME` | =0: off (default on): of the game's copy shader calls only the copies that change their destinations run (checked on thread bb:CopyFilter) |
| `BB_COPY_THREADS` | =N (0-16): threads for large guest memory copies |
| `BB_CORE_RESERVE` | =0 / 1: keep the game's threads off a reserved core (Linux, SMT CPUs with 6+ cores) |
| `BB_CRASH_GUARDS` | =0: without the guards of the game's own unchecked pointers that crashed players (scripts/crash_guards.py) |
| `BB_DEAD_IMAGES` | =0: images of freed memory left to the collector instead of dropped on the GPU thread |
| `BB_DEAD_IMAGES_LATE` | set: dead images dropped at the frame's end only, as before |
| `BB_DETILE_SKIPPED` | =1: texture levels the quality setting leaves out are still detiled and read |
| `BB_DIRECT_IMPORT_LOCKED` | =1: direct memory imports under the lock |
| `BB_DIRECT_MEMORY_ACCESS` | =1: shadPS4's direct memory access mode of the shader translator |
| `BB_DIRECT_SEGMENT` | =0: the presenter waits for the recording threads instead of taking its own command segment |
| `BB_DISABLE_HTTPS` | =1: the game's HTTPS requests go out as plain HTTP (online) |
| `BB_DRAW_PIPE` | =0 / N: the draw pipeline off, or its depth |
| `BB_FAST_WRITER_FAULTS` | =1: fast path for guest write faults (off: a device loss on a GTX 1050) |
| `BB_FAULT_WINDOW` | =KiB: window unprotected around a guest write fault (default 256) |
| `BB_FFX_OWN_SHADERS` | =0: FSR frame generation with the packaged shaders instead of the port's own |
| `BB_FG_ASYNC` | =1: FSR 3.1 frame generation on a second compute queue (no gain measured) |
| `BB_FG_EARLY_RETIRE` | =0: with frame generation the frame counts as finished only after it |
| `BB_FIXED_ARENA` | =1 / 0: force the fixed arena on or off (tests) |
| `BB_FIX_CHARA_ID` | =0: without the character id guard fix |
| `BB_FIX_HIT_COLLECTOR` | =0: without the hit collector overflow fix |
| `BB_FIX_PLAY_TIME` | =0: without the play time fix at high frame rates |
| `BB_FLIP_AHEAD` | =N (default 1, 0 off): the buffer shown before a flip goes back to the game that many flips early |
| `BB_FLIP_RATE` | =N: the flip rate the game asks for replaced (0: every vblank; games without frame rate hooks) |
| `BB_FRAMES_AHEAD` | =N (default 1): guest frames the command processor may run ahead of the GPU |
| `BB_FRAME_HOOKS` | =0: the community's frame rate patches instead of the port's hooks |
| `BB_FSR3_FP32` | =1: FSR 3.1 without its FP16 shader variants (#85) |
| `BB_FSR3_WAVE32` | =1: FSR 3.1 without its wave64 shader variants (#85) |
| `BB_FSR4_LINEAR` | =0: FSR 4 gets the encoded frame as before |
| `BB_FSR4_OPT` | =0: FSR 4 v07 passes without the optimized set (opt/) |
| `BB_FX_INIT_CLEAR` | =0: the effect buffers' initialization clears through the game's path |
| `BB_GARLIC_VRAM` | =0 / 1: VRAM copies of the game's GPU memory off / on (default: on discrete GPUs) |
| `BB_GC_BUDGET_MB` | =N: texture cache budget in MiB, as on integrated GPUs |
| `BB_GC_IDLE_SECONDS` | =N (default 20): unused textures evicted after N seconds |
| `BB_GC_KEEP_GPU_WRITTEN` | =0: the collector downloads and frees linear images the GPU wrote under mere memory pressure; default only past the critical mark and unused for a minute (#30) |
| `BB_GC_LIVE_BUDGET` | =0: texture cache marks from the startup budget instead of the driver's current one |
| `BB_GC_PRESSURE_IDLE_SECONDS` | =N (default 5): the same under memory pressure |
| `BB_GC_SMALL_MARKS` | =P,C: pressure and critical marks in percent on small cards |
| `BB_GPU_COMMAND_WRITES` | =0: GPU command writes not done in place |
| `BB_GPU_DATA_CHECK` | =0: GPU-written data not checked before CPU reads (#122) |
| `BB_GPU_LABELS` | =1 / 0: GPU-written labels on / off (default off on AMD GFX8) |
| `BB_GPU_LABELS_PORTABLE` | =1: portable GPU label writes |
| `BB_GPU_SPIN_US` | =N: microseconds the GPU thread spins before sleeping (default 200 with 12+ threads) |
| `BB_GPU_TIMESTAMPS` | =0: timestamps written by the CPU instead of the GPU |
| `BB_GPU_WRITES_IN_PLACE` | =1: GPU-written buffers in place with write tracking, as before |
| `BB_GPU_WRITE_TWINS` | =1: VRAM twins of GPU-written guest pages |
| `BB_GPU_WRITE_TWINS_MAX` | =N: pages with at most N GPU-written ranges get twins (default 1024) |
| `BB_GUEST_HOOKS` | =0: no guest code hooks at all (aspect ratio, HUD, mouse, fixes) |
| `BB_GUEST_LAZY` | =1: lazy guest memory with the layer's memory module (off: broke buffers) |
| `BB_GUEST_MEMORY` | =dmabuf / host / private: how guest memory is shared with the GPU (default per vendor) |
| `BB_GUEST_UNCACHED` | =1: guest memory only in memory types the GPU reads past its caches |
| `BB_GUEST_VRAM` | =1 (experiment): direct memory in VRAM mapped through the PCI BAR |
| `BB_GUEST_VRAM_RANGES` | =lo-hi,...: physical ranges for BB_GUEST_VRAM (default: the graphics heaps) |
| `BB_GX_COMMAND_HEAP` | =0: without the larger GX command heap patch (patches.py) |
| `BB_GX_QUERY_GUARD` | =0: without the GX query creation guard |
| `BB_HEAP_GROW` | =0: the graphics heap whole from the start instead of growing on demand |
| `BB_HEAP_GROW_AHEAD` | =N (default 2, 0 off): heap pieces made ahead |
| `BB_HEAP_GROW_INITIAL_MB` | =N (default 512): heap memory backed from the start |
| `BB_HEAP_GROW_PIECE_MB` | =N (default 320, at least 258): heap growth piece |
| `BB_HEAP_SHRINK` | =0: the heap only grows |
| `BB_HEAP_SHRINK_DELAY` | =seconds (default 60): a piece free this long is given back |
| `BB_HEAP_TOP_HOOK` | =0: without the hook on allocations from the heap's top |
| `BB_HIT_COLLECTOR_CAP` | =N (default 65536): hits per call before the overflow fix takes over |
| `BB_HONEST_DRAIN_FLUSH` | =0: no flush when the GPU is on its last submission |
| `BB_HONEST_FLIPS` | =1 (experiment): a flip completes once the GPU has finished the frame |
| `BB_HONEST_FLUSH_US` | =N (default 2000): submissions batched over N microseconds while the GPU has work |
| `BB_HONEST_IDLE_BACKLOG` | =bytes (default 64 KiB): queued draws that make an idle GPU get a flush |
| `BB_HONEST_IDLE_FLUSH` | =0: no flush for an idle GPU |
| `BB_HONEST_IDLE_US` | =N (default 200): microseconds of GPU idle before the guest may be waiting |
| `BB_HOST_COPY_WAITS` | =all: host copies always wait, as before |
| `BB_HOST_ONLY_DEFAULT` | =0: none of the game profile's allocations in host memory only |
| `BB_HOT_EFFECTS` | =0: effect settings applied as patches at start instead of live |
| `BB_HOT_PAGES` | =1: hot page tracking |
| `BB_HOT_VERTEX_COPY` | =1: vertex data bound in place copied into VRAM once per submission (no reads over the bus per pass) |
| `BB_IMAGE_RECYCLE_MB` | =N (default 256, 0 off): memory of freed images kept for reuse |
| `BB_IMAGE_RECYCLE_SECONDS` | =N (default 90): recycled images older than this go |
| `BB_IMAGE_SPARES` | =1 / 0: spare images made ahead on a thread (default on on Windows) |
| `BB_IMAGE_SPARES_MB` | =N: memory for spare images |
| `BB_IMAGE_TRAPS` | =0: no write traps on image memory |
| `BB_IMPORT_GRANULE_MB` | =N (2..64, default 32): lazy guest memory import granule |
| `BB_IMPORT_OVERLAP_MB` | =N: overlap budget of lazy imports |
| `BB_INDIRECT_GUARD` | =0: indirect draw and dispatch counts used as the game gives them |
| `BB_INDIRECT_GUARD_MAX` | =N (default 4M): indirect dispatch groups allowed in all |
| `BB_INDIRECT_GUARD_MAX_VERTICES` | =N (default 64M): indirect draws over this run nothing |
| `BB_INPLACE_TEXTURES` | =1: all texture data read in place (off: looked smeared) |
| `BB_INTERP_INT_FIX` | =0 / 1: flat integer interpolation fix off / on (default: on where barycentrics exist) |
| `BB_JOB_SLOT_FIX` | =0: without the render job slot leak fix |
| `BB_KEEP_DECOMPRESS_DRAWS` | =1: the game's depth decompress passes are drawn |
| `BB_KERNEL_CLAMP` | =0: without the effect kernel clamp fix |
| `BB_KIT_RESTORE` | =0: the online kit's code changes are not restored |
| `BB_LAYER_CPU_MARK_SECONDS` | =N (default 5, 0: until handed out): CPU marks of layer blocks expire after N seconds |
| `BB_LAYER_DOMAINS` | =0: mirrors not per game allocation |
| `BB_LAYER_GPU_WRITES` | =0 / 1 / 2: where GPU writes may land in VRAM copies (0 nowhere, 1 default) |
| `BB_LAYER_MIRROR_FLOOR_PERCENT` | =N (max 90): VRAM budget share kept for mirrors |
| `BB_LAYER_MIRROR_GROWTH_MB` | =N (default 64, 0 none): room a growing mirror gets |
| `BB_LAYER_MIRROR_HEADROOM_MB` | =N (0: the whole floor): mirror headroom |
| `BB_LAYER_READ_TRAPS` | =1: data the GPU wrote into a VRAM copy only is trapped for CPU reads |
| `BB_LAYER_RELOAD_RESET` | =0: blocks reloaded in place keep their state, as before |
| `BB_LAYER_STITCH` | =0: bindings across heap pieces from the arena, as before (exploding vertices) |
| `BB_LAYER_UNMAP_RESET` | =0: unmapped blocks keep their state, as before |
| `BB_LAYER_VOLATILE` | =1: volatile blocks |
| `BB_LAYER_VOLATILE_IN_PASS` | =0: volatile blocks refreshed in render passes too, as before |
| `BB_LAYER_WATCH` | =0: blocks with unannounced bytes get no watched VRAM copy |
| `BB_LAYER_WRITE_TRAPS` | =0: no write traps on layer blocks |
| `BB_LIGHT_GRID_HOOK` | =0: without the light grid hook |
| `BB_LIGHT_GRID_OPT` | =1 / 0: light grid optimization on / off, over the setting (tests) |
| `BB_LIVE_HYBRID` | =1: the startup resolution patch with live presets over it |
| `BB_MEM_SEMAPHORE_ORDER` | =0: memory semaphores without ordering |
| `BB_MIRROR_ALLOC_PERCENT` | =N (0 off): a mirror that would take device memory past N percent is refused |
| `BB_MIRROR_COMPACT_AGE` | =N (default 30): mirrors younger than N seconds are left alone |
| `BB_MIRROR_COMPACT_MB` | =N (default 64): mirror memory without valid blocks over this is given back |
| `BB_MIRROR_COMPACT_SECONDS` | =N (default 1): mirror compaction interval |
| `BB_MIRROR_FREE_BACKLOG_MB` | =N (default 128): retired mirror memory allowed to wait |
| `BB_MIRROR_FREE_MB_PER_FRAME` | =N (default 64): mirror memory freed per frame |
| `BB_MIRROR_MERGE_GROWTH` | =0: a mirror made over neighbours grows at the top only, as before |
| `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_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) |
| `BB_NP_COUNTRY` | =code: the online user's country (default us) |
| `BB_NP_LANGUAGE` | =code: the online user's language (default en) |
| `BB_NVIDIA_EGL` | =0: the launcher leaves the EGL vendor files as they are (#107) |
| `BB_NVIDIA_LIB_DIR` | =dir: extra folder with NVIDIA's libraries for the package |
| `BB_NVIDIA_MAILBOX` | set: keep Mailbox presentation on NVIDIA Windows instead of FIFO |
| `BB_NV_POWER` | =0: leave the NVIDIA driver profile of bb-probe.exe alone (Windows; by default its power management mode is set to prefer maximum performance unless the user chose one) |
| `BB_OCCLUSION_INTERVALS` | =0: occlusion samples counted all the time, not only while a query is open |
| `BB_OCCLUSION_LABEL_ORDER` | =0: occlusion labels may go ahead |
| `BB_OCCLUSION_QUERIES` | =0: a fixed occlusion result sequence instead of real queries |
| `BB_OF_FAST` | =1: optical flow without a search on the finest level |
| `BB_PACE_CATCHUP` | =1: frame pacing catches up after a late frame |
| `BB_PAD_CENTER` | =lx,ly,rx,ry: the sticks' neutral position |
| `BB_PAD_CENTER_CAL` | =0: no measurement of the sticks' neutral position |
| `BB_PAD_DEADZONE_RADIAL` | =0: the sticks' dead zone per axis instead of round |
| `BB_PAD_WORLD_DRAWS` | =N (default 150): a frame with this many draws counts as the world for pad timing |
| `BB_PAGED_CROSSING` | =0: paged bindings only in windows |
| `BB_PAGED_FORMATTED` | =0: formatted buffers never paged |
| `BB_PAGED_HOLES` | =1 (experiment): paged reads skip memory the game never wrote |
| `BB_PAGED_MIN_MB` | =N (default 64): bindings from this size are paged |
| `BB_PENDING_SUMMARY` | =0: pending checks without the summary level |
| `BB_PIPELINE_CACHE` | =0: no pipeline cache on disk |
| `BB_PIPE_SIGNALS` | =0: the frame's cache work on the GPU thread instead of the recording thread |
| `BB_PIPE_SPIN_US` | =N: microseconds the draw pipe's threads spin before sleeping |
| `BB_PM4_RECOVER` | =0: a command buffer the game rewrote while read stops decoding, as before |
| `BB_PM4_REPAIR` | =0: corrupt submitted command buffers decoded as they are |
| `BB_PM4_WAIT_US` | =N (default 20000): command processor wait timeout in microseconds |
| `BB_PRELOAD_BACKGROUND` | =0: cached pipelines loaded before the game starts instead of in the background |
| `BB_PREP_WORKERS` | =N (0-16): draw preparation threads |
| `BB_PRESENT_MODE` | =Mailbox / Fifo / Immediate: swapchain present mode (default Mailbox) |
| `BB_PRESENT_THREAD` | =0: present on the vblank thread instead of its own |
| `BB_PROMOTE_MB_PER_FRAME` | =N (0 none; default 32 on cards up to 4 GiB, else 256): VRAM copies per frame |
| `BB_READBACKS` | =0 / 1 / 2: GPU readbacks off / relaxed (default) / precise |
| `BB_READBACK_LINEAR` | =1: linear images read back to guest memory |
| `BB_READBACK_QUEUE` | =0: no separate queue for readbacks |
| `BB_REGS_FULL_RESET` | set: the whole register file reset, as before |
| `BB_RENDER_RESOURCE_CACHE_MB` | =N (160: the game's own): the game's render resource cache size |
| `BB_REPAIRED_DRAWS` | =1: repaired command buffers still draw |
| `BB_REPAIRED_WAIT_MS` | =N (default 500): waits of repaired command buffers give up after this |
| `BB_RESOLVE_WORKERS` | =N (0-4): threads that check and copy draw resources (stage 2) |
| `BB_RING_DEDUP` | =0: off (default on): constants already in the constant ring with the same bytes bound there again, not copied over the bus once more |
| `BB_SCENE_DEPTH_PROXIES` | =0: depth images read at the native size, not as reduced proxies |
| `BB_SCENE_STENCIL_ZERO` | =0: no depth proxies where stencil export is missing, the scene stays native |
| `BB_SCENE_UPSIZE` | =0: a scene larger than native drawn at the native size |
| `BB_SCENE_WITHOUT_UI` | =0: cinematics without a UI pass are not finished as scenes |
| `BB_SHADOWS` | =1: shadow copies of written buffers on discrete GPUs |
| `BB_SKIP_EMPTY_DRAWS` | =1 (experiment): draws that write nothing are left out |
| `BB_SKIP_NOOP_DRAWS` | =1: draws without effect left out (#352, no gain measured) |
| `BB_SOUND_HACK` | =0: saves left untouched (no sound settings fix) |
| `BB_SPEC_MATCH` | =0: shader specializations always built and compared |
| `BB_STAGE_PRIORITY` | =0 (default) / 1 / 2: draw stage thread priority off / above normal / highest |
| `BB_STAGING_KEEP_MB` | =N (default 512): upload staging kept when idle |
| `BB_STAGING_VRAM_IDLE_MS` | =N (default 3000): VRAM staging given back after this long unused |
| `BB_STREAM_BUFFER_MB` | =N: stream buffer size |
| `BB_SUBMIT_AHEAD` | =N (default 2): a frame's first submissions go straight to the GPU |
| `BB_SUBMIT_LOCK` | =frame: a frame's last submission decoded under the lock |
| `BB_SWAPCHAIN_IMAGES` | =N: swapchain images |
| `BB_TEXTURE_HELPER` | =0 / 1: spinning texture helper thread (default: with cores to spare) |
| `BB_TEXTURE_UPLOAD_MB_PER_FRAME` | =N (default 32, 0 off): texture bytes uploaded a frame before new textures start small |
| `BB_THD_AUTH` | The Hunter's Dream account service URL |
| `BB_THD_P2P_ADDR` | our address for The Hunter's Dream peers (default: detected) |
| `BB_THD_RULESET` | The Hunter's Dream ruleset (default vanilla) |
| `BB_THD_SERVER` | The Hunter's Dream session service URL |
| `BB_THD_STUN` | =host[:port] / off: STUN server (default: the server's host on 3478) |
| `BB_THD_VERIFY_TLS` | =0: The Hunter's Dream TLS certificates not checked |
| `BB_TILE_BIND_VRAM` | =0: tiled data read in place over the bus instead of from a VRAM copy |
| `BB_TIMELINE_POLL_US` | =N (default 50): timeline semaphore poll interval |
| `BB_UNIFIED_MEMORY` | =1 / 0: treat the GPU as unified memory or not (over the device type) |
| `BB_VBAO_BLUR` | =N (default 2, 0: raw): VBAO denoise taps per side |
| `BB_VBAO_BLUR_DEPTH` | =f (default 0.05): relative depth difference that stops the VBAO blur |
| `BB_VBAO_BLUR_NORMAL` | =f (default 8, 0 ignores normals): how sharply the VBAO blur stops at normal changes |
| `BB_VBAO_NORMALS` | =0: VBAO from depth only |
| `BB_VBAO_POWER` | =f (default 1.5): VBAO strength |
| `BB_VBAO_RADIUS` | =metres (default 1.5): VBAO radius |
| `BB_VBAO_THICKNESS` | =metres (default 0.4): VBAO assumed thickness |
| `BB_VERTEX_READ_COPY` | =1 (experimental): vertex data bound in place copied into VRAM for the vertices each draw reads (once per guest submission) |
| `BB_VK_RECORD_THREAD` | =0: Vulkan commands recorded on the calling thread |
| `BB_VK_RECORD_THREADS` | =N (1-8): Vulkan recording threads (default 2 from 8 hardware threads) |
| `BB_VK_SEGMENT_KB` | =N (default 64): recording segment size |
| `BB_VMA_BLOCK_MB` | =N (default 64): VMA block size |
| `BB_VMA_DEDICATED_MB` | =N (default 4): images from this size get dedicated memory |
| `BB_VRAM_FLOOR_IDLE_SECONDS` | =N (default 30): mirror floor idle time before textures make room |
| `BB_VRAM_IDLE_SECONDS` | =N (default 60): VRAM copies unused this long are given back |
| `BB_VRAM_LIMIT_MB` | =N: the driver's VRAM budget taken as at most N MiB |
| `BB_VRAM_PRESSURE_IDLE_SECONDS` | =N (default 10): under pressure, VRAM groups unused this long go |
| `BB_VRAM_PROMOTE_PERCENT` | =N (default 90): no new VRAM copies past this share of the budget |
| `BB_VULKANINFO` | path of vulkaninfo for the launcher |
| `BB_WORK_RETIRED` | =0: the recording thread and fences do not wait for retired work |
## Diagnostics (282)
Logs, dumps, traces, probes and experiments, for finding bugs. Most cost frame time; none is meant for playing.
| Variable | What it does |
|---|---|
| `BB_ARENA_VRAM_ONLY` | =1 (tests): the arena in VRAM only, as NVIDIA's sparse buffers |
| `BB_ATLAS_DEBUG` | =1: Hunter's Atlas logs what its game-state readers find |
| `BB_ATLAS_QA` | =1: Hunter's Atlas's test commands (BB_MAP_TEST_CONTROL) may also send mouse, key and resize events |
| `BB_ATLAS_QA_DEVICE` | =controller / keyboard: with BB_ATLAS_QA, the prompts Hunter's Atlas shows |
| `BB_AUDIO_DUMP` | =dir: raw converted PCM per audio port |
| `BB_AUDIO_STATS` | set: audio port statistics |
| `BB_AUDIO_TRACE` | =1: Ajm instance configurations and jobs that fail |
| `BB_AVPLAYER_TRACE` | =1: the game's movie player calls with results and times |
| `BB_BARRIER_STATS` | =1: images whose subresource states cost the most |
| `BB_BBSC_MEMSCAN` | set: BBSC finds its sites by scanning memory instead of the known addresses |
| `BB_BBSC_PROBE` | =address: BBSC probe of that guest address |
| `BB_BDA_LOG` | set: GPU addresses of buffer sources, to place a GPUVM fault |
| `BB_BIG_ALLOC_STACKS` | =1: call stacks of device memory allocations of 64 MiB and more |
| `BB_BREADCRUMB_READS` | =1 / 0: breadcrumbs of T# and V# reads everywhere / nowhere (default: NVIDIA only) |
| `BB_BUFFER_STATS` | =1: how many frames behind the GPU is when a frame starts |
| `BB_CAMERA_FRAME_LOG` | set: projection of every G-buffer pass's camera with its GPU frame |
| `BB_CAMERA_INVERSE` | =own: motion vectors from the inverse of the view matrix instead of the game's stored one |
| `BB_CAMERA_LOG` | set: camera position every 100 ms |
| `BB_CAMERA_Y` | =up / down: force the camera's y direction for motion vectors |
| `BB_CAMLOG` | set: camera yaw when it changes (#325) |
| `BB_CAPTURE_DIR` | folder for frame captures (BB_CAPTURE_TRIGGER) |
| `BB_CAPTURE_TRIGGER` | =file: creating that file records the next frame for the frame analyzer |
| `BB_CE_STATS` | =1: what the constant engine's dumps feed |
| `BB_CE_WAIT_LOG` | set: constant engine waits that were not satisfied |
| `BB_CE_WATCH` | =hex address: constant RAM dumps and WRITE_DATA packets over that address |
| `BB_CLOTH_STIFFNESS` | =value: cloth stiffness bound at any frame rate |
| `BB_COMPILE_TRACE` | =1: every shader and pipeline compiled during the session |
| `BB_CONSTRAM_WAIT_LOG` | set: constant RAM dumps that waited for the GPU, sampled |
| `BB_COPY_SHADER_TRACE` | set: the copy shader's buffers every 2 s |
| `BB_COPY_WHERE` | =1: the copy shader's sources and destinations per frame, VRAM or in place (log lines) |
| `BB_DCB_STATS` | =1: structure of the graphics command buffers |
| `BB_DEBUG_MOTION` | =1: motion vectors blended over the picture |
| `BB_DISPATCH_LOG` | =1: the shape of each kind of compute dispatch, once |
| `BB_DISPATCH_START_LOG` | set: dispatches with a nonzero thread group start |
| `BB_DISPLAY_WATCH` | set: every draw into a display buffer with its sizes and state |
| `BB_DISPLAY_WATCH_TARGET` | =hex address: also every draw into that color target |
| `BB_DLSSG_CAMERA_MOTION` | =0: DLSS-G told the vectors have no camera motion |
| `BB_DLSSG_DEPTH_INVERTED` | =1: DLSS-G told the depth is the other way round |
| `BB_DLSSG_HUDLESS` | =0: DLSS-G gets no hudless frame |
| `BB_DLSSG_MATRICES` | =0: DLSS-G gets no camera matrices |
| `BB_DLSSG_MVEC_SCALE` | =f: DLSS-G motion vector scale (default 1) |
| `BB_DLSSG_PREFILL` | =0: DLSS-G outputs not filled with the real frame first (black frames when it skips) |
| `BB_DLSSG_SUBRECTS` | =0: DLSS-G gets no input sub-rectangles |
| `BB_DRAW_WATCH` | =1: draws whose viewport, targets or textures differ from the previous frame's |
| `BB_DRAW_WATCH_TEXTURES` | set: BB_DRAW_WATCH also lists bound textures |
| `BB_DUMP_DIR` | =dir: where upscaler dumps go (default out/dump) |
| `BB_DUMP_EVERY` | set: dump every frame while the trigger file exists |
| `BB_DUMP_FRAMES` | =N: frames per upscaler dump (default 8) |
| `BB_DUMP_GOT` | =offset: which host function a guest GOT slot calls |
| `BB_DUMP_ONLY` | =name,name: dump only those upscaler images |
| `BB_DUMP_SHADERS` | =1: translated shader programs written to the dump folder |
| `BB_DUMP_TRIGGER` | =file: creating it dumps the upscaler's inputs and outputs |
| `BB_DYNAMIC_STATE_ALWAYS` | =1: all dynamic state set again at every draw |
| `BB_DYNSTATE_CHECK` | =1: the scissor each command buffer really has, and who set it |
| `BB_EFFECT_DUMP` | set: the game's effect options objects every 10 s |
| `BB_EXPERIMENT_CYCLE` | =bits,bits,...:seconds: the experiment bits step through the list, over and over (test builds) |
| `BB_EXP_BARRIER_ALL` | set (experiment): a full memory barrier before every draw and dispatch |
| `BB_EXP_BARRIER_TARGET` | =hex address (experiment): a full barrier before each draw into that target |
| `BB_EXP_CONSTRAM_FINISH` | =1 (experiment): every constant RAM dump waits for all earlier GPU work |
| `BB_EXP_COPY_SRC_SNAPSHOT` | set (experiment): copy shader sources snapshotted as with the old memory model |
| `BB_EXP_CPU_READS_GPU_WAIT` | set (experiment): CPU-side reads also wait for the GPU, as before |
| `BB_EXP_DMA_UNTRACKED` | set (experiment): stages reading memory through pointers count as unlisted reads |
| `BB_EXP_EXTRA_BREAKS` | =N (experiment): the render pass ended before every Nth draw, to measure breaks |
| `BB_EXP_FINISH_BEFORE_TARGET` | =hex address (experiment): the GPU finishes everything before a draw into that target |
| `BB_EXP_FINISH_FRAMES` | set (experiment): the GPU finishes each frame before the next |
| `BB_EXP_FLIP_FINISH` | set (experiment): the flip is signalled once the GPU has run the frame |
| `BB_EXP_FLIP_LAG` | set (experiment): at most one frame in flight |
| `BB_EXP_NOTED_GPU_WAIT` | set (experiment): constant RAM waits cover every read noted on stage A, as before |
| `BB_EXP_NO_MEMORY_HOOKS` | set (experiment): no guest hooks for GPU allocations and copies |
| `BB_EXP_NO_STREAM` | set (experiment): nothing goes through the stream buffer |
| `BB_EXP_PAGED_IN_PLACE` | =1 (experiment): paged buffers bound in place |
| `BB_EXP_TRANSIT_TARGET` | =hex address (experiment): that target goes through TransferSrcOptimal |
| `BB_FAULT_WAIT` | =1: a fatal fault or a GPU library assertion leaves the process alive for a debugger (tools/debug_attach.sh) |
| `BB_FENCE_DELAY_TEST` | =1 (test): every 200th fence comes 80 ms late |
| `BB_FFX_FG_DEBUG` | =1: AMD's frame generation draws its tear lines and reset marks |
| `BB_FG_DUMP_TRIGGER` | =file: creating it saves real and generated frames, raw |
| `BB_FG_LUT` | =0: generated frames without the game's display LUT |
| `BB_FG_PROFILE` | =1: GPU time of each FSR 3.1 frame generation pass |
| `BB_FILE_TRACE` | =1: every guest path the game opens or checks (mods, missing files) |
| `BB_FINAL_DUMP_TRIGGER` | =file: creating it saves the presented frame |
| `BB_FI_CHECK` | =1: frame interpolation samples drawn as their difference to the SDK's |
| `BB_FPS_PRINT` | =1: the present rate every 5 s, without BB_FRAME_STATS's cost |
| `BB_FRAME_DT_SMOOTH` | =a (0..1): the game's frame step follows the measured frame time smoothed by a |
| `BB_FRAME_GEN_ONLY` | set: only the generated frames are shown |
| `BB_FRAME_LOG` | =path (with BB_FRAME_STATS): one line per flip |
| `BB_FREE_CHECK` | =1: guest heap corruption diagnostic around the free list |
| `BB_FSR411_POSTPASS` | =lds / orig: FSR 4.1.1 post pass variant |
| `BB_FSR4_MODEL_CYCLE` | =N: the FSR 4 model switched to the next one every N frames |
| `BB_FSR4_PROFILE` | =1: FSR 4.1.1 GPU time per pass every 300 frames |
| `BB_FSR4_STATS` | =1: driver statistics of every FSR 4 pass (fsr4-bench --stats) |
| `BB_GAME_ASSERT_STOP` | =1: the game's known harmless asserts stop it, as before |
| `BB_GAME_HUGE` | =0 (tests): no huge pages for the game's image |
| `BB_GAME_MENU_TRACE` | =1: how often the game asks for the port's menu texts |
| `BB_GAME_PROFILE` | =none: no game profile (hooks, fixes), the plain runtime |
| `BB_GARLIC_VRAM_RANGE` | =begin:end: VRAM copies only for guest addresses in that range |
| `BB_GC_TRIGGER_MB` | =N: the texture collector starts at N MiB (tests) |
| `BB_GNM_CENSUS` | =1: calls per Gnm function, printed |
| `BB_GNM_CENSUS_FILE` | =path: every Gnm function's total calls written there |
| `BB_GNM_CENSUS_RANGE` | =first:last: census only for those table indices (bisecting) |
| `BB_GNM_NATIVE_ONLY` | =1: native Gnm observers without checksum analysis |
| `BB_GNM_OBSERVE` | =1: submitted command buffers checked against the hooked API calls' packets |
| `BB_GNM_RACE_STACKS` | =1: call stacks of racing Gnm command buffer writers |
| `BB_GNM_TRACE` | =offset[:count]: arguments, caller and packets of that Gnm function |
| `BB_GNM_WRITES_TRACE` | =1: recent Gnm builders overlapping a bad submitted buffer |
| `BB_GPU_LOG` | =trace / debug / info / warning / error: GPU layer log level |
| `BB_GPU_PROFILE_DRAWS` | =1: the GPU profiler times every draw |
| `BB_GPU_PROFILE_TOP` | =N: the GPU profiler prints the N heaviest labels (default 30) |
| `BB_GPU_WRITE_WATCH` | =hex address: GPU writes noted over the 64 KiB page holding it |
| `BB_GUEST_HOOK_TRAPS` | =1: guest hooks stay int3 traps instead of native detours |
| `BB_GUEST_WHOLE_CHUNKS` | =1 (tests): as if offset mappings failed (the NVIDIA path) |
| `BB_GX_QUERY_FAIL` | =N: every Nth GX query creation fails as with a full heap |
| `BB_HEAP_DUMP` | =addr,...: those guest objects and the ones they point to |
| `BB_HEAP_FIND` | =hex value: once, 60 s in, where the game's data holds that value |
| `BB_HEAP_HOOKS` | =0 (bisecting): the heap's ranges not reported |
| `BB_HEAP_SITES` | =1: the game's malloc replacement sites |
| `BB_HEAP_TRACE` | =1: which 16 MiB ranges the game's GPU allocators hand out, every 60 s |
| `BB_HEAP_WALK` | =core,...: walk of the game's range allocator cores |
| `BB_HLE_OFF` | =mask (experiment): 1 image copy, 2 metadata clear, 4 image clear run as real shaders |
| `BB_HLE_STATS` | set: how often the compute shader substitutions fire |
| `BB_HOST_ONLY` | =phys,... (experiment): those direct allocations in host memory only |
| `BB_HUD_DX` | =f: another HUD shift |
| `BB_IMAGE_CLEAR_LOG` | =1: why an image clear went to the real shader, once per reason |
| `BB_IMAGE_COPY_LOG` | set: also why a large copy shader was not substituted |
| `BB_IMAGE_SYNC_LOG` | set: images refreshed most per 5 s, with their flags |
| `BB_IMAGE_WATCH` | =address: what happens to the image at that address |
| `BB_IME_TEXT` | =text: the on-screen keyboard answers with that text at once (tests) |
| `BB_IMITATE_NVIDIA` | =1 (tests on AMD): the port treats this GPU as NVIDIA's |
| `BB_IMPORT_LOG` | set: lazy import requests per 10 s |
| `BB_IMPORT_TRACE` | =lo-hi (MiB): who asked for imports starting there |
| `BB_IMPORT_WRITTEN_BUDGET` | =1 (tests): written bindings under the import budget too, as before |
| `BB_INPLACE_READ_CHECK` | =1: checks of the GPU's in-place reads of game memory |
| `BB_LABEL_TRAP` | =1 / 2: with BB_FREE_CHECK, read-only label pages first |
| `BB_LANG_TABLES` | set: the sell regions' language tables (#269) |
| `BB_LAYER_CHECK` | =1: stop on a stale page table entry of the layer |
| `BB_LAYER_MARK_LOG` | =lo-hi (hex): who marks layer blocks there as the CPU's |
| `BB_LAYER_PAGED` | =0 (tests): huge bindings cut in place instead of the page table |
| `BB_LAYER_PAGED_STATS` | set: paged bindings per shader every 5 s |
| `BB_LAYER_PROBE` | =1: GPU writes kept in place because the CPU once wrote the block |
| `BB_LAYER_VERIFY` | =1 / 2: verify the layer's mirrors (2: also those a paged binding reads) |
| `BB_LAYER_WATCH_HOT` | =1 (experiment): often written blocks watched and uploaded after every write |
| `BB_LAYER_WRITE_LOG` | set: large GPU writes kept in place, and the images near them |
| `BB_LIST_UNRESOLVED` | =1 (porting another game): every unresolved import, once |
| `BB_LOW_INFO_TEST` | set: test of the memory queries for the game's statics (#282) |
| `BB_MAP_TEST_CONTROL` | =file: one command at a time for Hunter's Atlas's automated tests (open, close, zoom-in, area <id>, ...), removed once read |
| `BB_MAP_TEST_TRACE` | =file: with BB_MAP_TEST_CONTROL, Hunter's Atlas writes its state there as JSON |
| `BB_MEMORY_LOCK_REPORT_MS` | =N (default 20, 0 off): memory map lock holds longer than this, with the code |
| `BB_MENU_KEYS_FILE` | =file (scripted tests): menu key tokens read from it |
| `BB_MENU_POINTER_FILE` | =file (tests): menu pointer moves and clicks read from it |
| `BB_MENU_POINTER_LOG` | =1: the signals of the in-menu check, with every change |
| `BB_META_FIXED_BASE` | set: shader prediction from metadata at a fixed program base |
| `BB_META_NO_SYNTH` | set: shader prediction without synthesized inputs |
| `BB_META_RECIPE_FETCH` | set: shader prediction without generated fetch shaders |
| `BB_MIRROR_TRACE` | set: mirror creation and growth |
| `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) |
| `BB_OBJECT_MOTION_ALL` | =1: the old broad object motion selection (A/B) |
| `BB_OCCLUSION_READ_TRACE` | =1: the game's occlusion result reads |
| `BB_OCCLUSION_SAMPLES` | =N: samples the game read, for N queries |
| `BB_OCCLUSION_TRACE` | =N: the first N occlusion events with frame and render pass |
| `BB_OVERLAY_OPEN` | set (tests): the port's menu open from the start, unfolded |
| `BB_OVERLAY_TRACE` | =1: the menu's mouse buttons as SDL and ImGui see them |
| `BB_PAD_CLOCK` | =wall: pad timestamps from the wall clock, as before |
| `BB_PAD_FILE` | =file: scripted pad input for automated runs |
| `BB_PAD_RECORD` | =file: F9 starts and stops recording the pad state into it |
| `BB_PAD_REPLAY` | =file: plays a BB_PAD_RECORD recording |
| `BB_PAD_TAP_TEST` | =ms: the right stick fully right for that long (#325) |
| `BB_PAD_TRACE` | =1 / 2: gamepad buttons and touches as read (2: every sample) |
| `BB_PAGED_COPY_CROSSING` | =0 (tests): copy programs paged for size alone are not paged |
| `BB_PAGED_VERIFY` | =1: verify every block a paged binding reads |
| `BB_PARTIAL_TRACE` | set: partial image refreshes |
| `BB_PASS_BREAK_TRACE` | =1: what ended each render pass |
| `BB_PASS_DUMP` | =addr,...: rows of those render targets at each display pass (with BB_PASS_DUMP_TRIGGER) |
| `BB_PASS_DUMP_AT` | =hex address: BB_PASS_DUMP's images also before the first draw into that target |
| `BB_PASS_DUMP_STEP` | =N (default 8): BB_PASS_DUMP every Nth row |
| `BB_PASS_DUMP_TRIGGER` | =file: creating it starts BB_PASS_DUMP |
| `BB_PENDING_STATS` | set: pending notes and granules a second, by level |
| `BB_PIN_CLEAR_LOG` | set: large releases of GPU-pinned memory that were cleared |
| `BB_PIPE_VERIFY` | =N: verify the draw pipe's results every Nth frame |
| `BB_PM4_SELFTEST` | =1: tests of command processor packets Bloodborne never sends |
| `BB_PM4_SELFTEST_STEPS` | =mask: the self test steps to run |
| `BB_POOL_WATCH` | =1: watch of the game's pool corruption (#32) |
| `BB_PRESENT_DUMP_COUNT` | =N: BB_PRESENT_DUMP_TRIGGER saves N consecutive frames |
| `BB_PRESENT_DUMP_TRIGGER` | =file: creating it saves the presented frame with the UI |
| `BB_PRESET_FILE` | =file: 'preset [output [upscaler [aspect]]]' read about once a second (scripted tests) |
| `BB_PRODUCER_CHECK` | =1: command producer checks (the launcher's crash diagnostics) |
| `BB_PROMPT_TRACE` | =1: key guide icon textures found for button prompts |
| `BB_READBACK_LOG` | =path: the newest GPU-written bindings behind readbacks |
| `BB_READ_SAMPLE` | =spec: sampling of which guest memory the CPU reads |
| `BB_RECORDER_TRACE` | =1: most frequent command buffer callers every 2000 calls |
| `BB_REGS_CHECK` | set: registers compared with the defaults after a reset |
| `BB_RESIDENCY_MIX` | =1 / 2: bindings per kind every 2 s |
| `BB_RESIDENCY_REPORT` | =file: when it appears, blocks bound in place and why they have no VRAM copy |
| `BB_RESIDENCY_VRAM_LIMIT_MB` | =N (tests): pretend VRAM ends at N MiB |
| `BB_RES_STATS` | =1: where bound shaders read their resource descriptors from |
| `BB_RE_ADVANCE` | =1: probe of Scaleform's MovieImpl::Advance |
| `BB_RE_ANALOG` | =1: probe of the analog getter |
| `BB_RE_CAMERA` | =1: camera probe |
| `BB_RE_CAMSTICK` | =1: probe of the camera stick getter |
| `BB_RE_CAM_ADD` | =radians: turns the camera by that much (probe) |
| `BB_RE_ENTRY` | =offset,...: entry probes at those guest offsets |
| `BB_RE_GX_QUERY` | =1: first GX query creations and guard events with their thread |
| `BB_RE_VIEWPORT` | =1: the game's viewport calls |
| `BB_RE_WATCH` | =offset: hardware watchpoint on that guest offset |
| `BB_RE_WATCH_ADDR` | =address: hardware watchpoint on that address |
| `BB_RE_WATCH_AT` | =N (default 3000): watchpoint set after that many pad reads |
| `BB_RE_WATCH_CAMANGLE` | =1: watch who writes the camera angle |
| `BB_RE_WATCH_CAMIN` | =1: watchpoint on the camera input |
| `BB_RE_WATCH_CAMSTATE` | =1: watch who reads the camera's aspect ratio |
| `BB_RE_WATCH_FILE` | =file: watch the hex address written in it once it appears |
| `BB_RE_WATCH_FOLLOW` | =N: follow the watched bytes through host copies N times |
| `BB_RE_WATCH_INDEX` | =N (default 62): virtual input index for BB_RE_WATCH_VIRTUAL |
| `BB_RE_WATCH_VIRTUAL` | =1: watchpoint on the virtual right stick X |
| `BB_RE_ZERO_VIRTUAL` | =1: virtual inputs fed from the right stick's X get 0 |
| `BB_RING_REASONS` | set: why small read-only bindings were not copied, per 5 s |
| `BB_ROBUSTNESS` | =0 (experiment): buffer and image robustness off |
| `BB_SAMPLE_ALL` | =1: every sampled address as module+offset |
| `BB_SAMPLE_RANGE` | =lo-hi: guest offsets the sampler reports |
| `BB_SAMPLE_REPORT_SECONDS` | =N: thread samples every N seconds |
| `BB_SAMPLE_THREAD` | =name: where that thread runs, sampled |
| `BB_SAVE_TRACE` | =1: every operation on save files |
| `BB_SCENE_DEBUG` | =file: when it appears, where the scene image is accessed |
| `BB_SCENE_NO_RESOLVE` | =file:line,...: those accesses read the native scene image |
| `BB_SCENE_SAMPLED_RESOLVE` | =0: shader reads take the native scene image as it is |
| `BB_SCENE_STENCIL_BITS` | =1 (tests): the eight-draw stencil reconstruction |
| `BB_SECTIONS` | =1: timing of the draw recording thread's sections |
| `BB_SECTIONS_SLOW` | =1 (with BB_SECTIONS): slow frames' sections at each flip |
| `BB_SEQUENCE_DUMP_COUNT` | =N (default 16): images per BB_SEQUENCE_DUMP_TRIGGER |
| `BB_SEQUENCE_DUMP_TRIGGER` | =file: creating it saves the next images shown (frame generation checks) |
| `BB_SHADER_CAPTURE` | =dir: shader inputs captured there |
| `BB_SKIP_PS` | =hash,...: draws with these pixel shaders left out |
| `BB_SLOW_BIND_LOG` | =microseconds: image memory binds slower than this |
| `BB_SPEC_MATCH_CHECK` | set: the built specialization must find the same one |
| `BB_STALL_REPORT` | =0: no report when the queues stop making progress |
| `BB_STALL_THREADS` | =0: a stall report covers the command thread only |
| `BB_STREAM_STATS` | =1: what a frame streams |
| `BB_TAA_DIAGNOSTICS` | set: TAA diagnostic image dumps |
| `BB_TEST_VPAD` | =1 (tests): a virtual Xbox-like gamepad |
| `BB_THD_TRACE` | =1: every The Hunter's Dream datagram and callback |
| `BB_TICK_LOG` | set: ranges marked written by the GPU most often, per 5 s |
| `BB_TILE_BACK_LOG` | set: which shader a 'Tile back' line is for |
| `BB_TIMELINE` | =file: a timeline of frames on their way to the GPU |
| `BB_TIMELINE_START` | =seconds (default 30): BB_TIMELINE starts recording this late |
| `BB_TLS_WATCH` | =offset: who writes that static TLS variable of the main thread |
| `BB_TOGGLE_FILE` | =file: experiment bits re-read every 250 ms |
| `BB_TRACE_ADDR` | =addr:size: that range traced from the start (with BB_TRACE_SIZE) |
| `BB_TRACE_CB` | =hash: how that shader's constant buffers were bound |
| `BB_TRACE_CB_LIST` | =hash,...: BB_TRACE_CB for several shaders |
| `BB_TRACE_CB_RANGE` | =lo-hi: constant buffer bindings over that range |
| `BB_TRACE_GPU_INPUTS` | =vs hash: the traced draw's buffer ranges, dumped at the display pass |
| `BB_TRACE_PIPELINE` | =vs hash: that draw's pipeline handle and push data |
| `BB_TRACE_SEMA` | set: blocking semaphore waits (deadlocks) |
| `BB_TRACE_SHADER` | =vs hash: that draw's counts and streams once a second |
| `BB_TRACE_SIZE` | =bytes: bindings over GPU-written buffers of that size, counted |
| `BB_TRACE_VERTEX_DATA` | set (with BB_TRACE_SHADER): the stream bytes whenever they change |
| `BB_TROPHY_PREVIEW` | =id: at the game's first trophy call, that trophy's notice without recording it |
| `BB_TROPHY_TEST` | =id: that trophy's announcement once, nothing recorded |
| `BB_UI_DRAWS` | =file: while it exists, one frame's UI draws (another aspect ratio, or the upscaler's UI phase) logged with their targets, stencil, blend, clip rects and redirection |
| `BB_UI_DRAWS_PS` | =hex pixel shader hash: BB_UI_DRAWS also dumps that shader's draws' vertex streams, first constant buffer and indices |
| `BB_UI_TEXTURE_LOG` | =file: when it appears, the textures the UI binds over the next 2 s |
| `BB_UI_TRACE` | set: draws of frames that ended without the output-size UI |
| `BB_UI_TRIGGER_VS` | =hex hash: the vertex shader that starts the UI phase |
| `BB_UPSCALE_BEFORE_CS` | =hex hash: upscale before that compute shader |
| `BB_VERTEX_IN_PLACE` | =1: vertex data bound in place per frame, by why it has no VRAM copy, and what draws read of it (log lines) |
| `BB_VK_MARKERS` | =1: the game's debug labels as Vulkan markers |
| `BB_VK_VALIDATION` | =1: Vulkan validation layers |
| `BB_VK_VALIDATION_SYNC` | =1: Vulkan synchronization validation |
| `BB_VRAM_ACCESS_TRAP` | set: blocks moved to VRAM are closed to the CPU |
| `BB_VRAM_TRAP_RANGE` | =addr:size: BB_VRAM_ACCESS_TRAP only over that range |
| `BB_WAIT_LOG` | =file: the game's kernel waits written there |
| `BB_WAIT_TRACE` | =1: where the time waiting for GPU ticks goes |
| `BB_WARN_META_TEXTURE` | set: warn about textures read from metadata memory |
| `BB_WATCH` | =addr:size: what the translator reads from that range |
| `BB_WRITE_LOG` | =1: GPU writes to guest memory near a fault |
| `BB_WRITE_SHADOWS` | =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_WRITE_SHADOW_TRACE` | =1: why BB_WRITE_SHADOWS lookups miss and who drops the copies (log lines) |
| `BB_WRITE_VERIFY` | =1: page protection to catch writes nothing announced |
| `BB_ZERO_SCAN` | =seconds: that long after the first mapping, how much direct memory is zero |
## Internal (44)
Passed between the launchers, run.py / run.sh and the game. Not meant to be set by hand.
| Variable | What it does |
|---|---|
| `BB_BUNDLED_VK_DRIVER_FILES` | the package's own Vulkan driver manifests (set by the launcher) |
| `BB_BUNDLED_VK_LAYER_PATH` | the package's own Vulkan layers, found before the host's (set by the launcher) |
| `BB_CLIENT_VERSION` | the online client's name for the server (bbport VERSION, set by run.py) |
| `BB_DATA_DIR` | the port's data folder (set by run.py) |
| `BB_DEBUG_KEEP` | =1: tools/debug_attach.sh leaves the process stopped after writing its report (--keep) |
| `BB_DEBUG_OUT` | where tools/debug_attach.sh writes its report (set by the script) |
| `BB_DLSS_DIR` | the DLSS bridge and NVIDIA's DLSS libraries' folder in place of bb-probe's (set by the AppImage without Nix) |
| `BB_FSR411_OUT` | where the launcher's FSR 4.1.1 build puts its assets |
| `BB_FSR4CAP_CLEAN` | =1: the FSR 4.1.1 build removes its work folder afterwards (set by the launcher) |
| `BB_FSR4CAP_TOOLS` | the FSR 4.1.1 build's tools folder (set by the launcher) |
| `BB_FSR4CAP_WORK` | the FSR 4.1.1 build's work folder (set by the launcher) |
| `BB_GAMEPAD_STRICT` | =1 (local co-op): only the chosen gamepad, never another one instead |
| `BB_GPU_USER_DIR` | the user folder as the GPU library and the online module see it (set by the runtime) |
| `BB_HOST_VK_ADD_DRIVER_FILES` | the host's VK_ADD_DRIVER_FILES under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
| `BB_HOST_VK_ADD_LAYER_PATH` | the host's VK_ADD_LAYER_PATH under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
| `BB_HOST_VK_DRIVER_FILES` | the host's VK_DRIVER_FILES under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
| `BB_HOST_VK_ICD_FILENAMES` | the host's VK_ICD_FILENAMES under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
| `BB_HOST_VK_IMPLICIT_LAYER_PATH` | the host's VK_IMPLICIT_LAYER_PATH under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
| `BB_HOST_VK_LAYER_PATH` | the host's VK_LAYER_PATH under Steam's pressure-vessel, kept for the log while the AppImage without Nix drops it |
| `BB_IMAGE_SHAPES` | the package's image shapes list for spare images (set by run.py) |
| `BB_INTERNAL_HEIGHT` | the game's internal screen height (default 1080; set by run.py) |
| `BB_INTERNAL_WIDTH` | the game's internal screen width (default 1920; set by run.py) |
| `BB_LIVE_RES` | =1: render resolution changes live (set by run.py) |
| `BB_MODS_CONFIG` | the mods list (default <data>/mods.json) |
| `BB_MODS_DIR` | the folder of enabled mods' files (set by run.py) |
| `BB_MONITOR_SIZE` | =WxH: the monitor's size the startup patch was made for (set by the launcher) |
| `BB_OUTPUT_RES` | =WxH: the output the startup patch was made for (set by run.py) |
| `BB_PATCHES_CONFIG` | the patch choice file (default <data>/patches.json) |
| `BB_PATCHES_DIR` | the patches folder (default <data>/patches) |
| `BB_PKGEXTRACT` | pkgextract for the launcher's Install from PKG (the AppImage's copy) |
| `BB_PREBUILT` | =1: packaged build, run.sh builds nothing (set by the AppImage) |
| `BB_PRESENCE_DIR` | Discord Rich Presence's folder for this start (run.sh; scripts/discord_presence.py) |
| `BB_PROBE` | path of bb-probe for run.py |
| `BB_PROMPT_ART` | the Xbox and Switch button art for the live prompts, KG_*.rgba by style (scripts/prompt_art.py; set by run.sh) |
| `BB_SAVEDATA_TITLE` | the saves' title id (set by the runtime), for BBSC's save folder before the game has written one |
| `BB_SHADER_GAME_DIR` | the game's files the shader recipes' programs come from (set by run.py) |
| `BB_SHADER_RECIPES` | the shader recipes pack (set by run.py) |
| `BB_SKIP_NETWORK_CHOICE` | =1: no online choice at start (the launcher's) |
| `BB_STALL_THREADS_DONE` | set by the port once all threads were reported |
| `BB_STEAM_OVERLAY` | the Steam overlay's preload kept for the game (=0: off; set by the AppImage entry) |
| `BB_TIMEOUT` | =seconds: run.py stops the game after this long (tests) |
| `BB_TROPHY_DIR` | the trophy list and icons scripts/trophies.py prepared, where the earned ones are recorded (set by run.sh) |
| `BB_USER_DIR` | the user folder (saves, settings; set by run.py) |
| `BB_VBLANK_HZ` | =N (default 60, 0: 480): the guest vblank rate (1000 for uncapped; set by run.py) |
## Build (15)
Read by the build and packaging scripts only.
| Variable | What it does |
|---|---|
| `BB_FSR4CAP_CHECKED` | internal to tools/fsr4cap: the tools were checked |
| `BB_FSR4CAP_FP8` | =1: also the FP8 FSR 4.1.1 variant through Proton's emulation; 0: never |
| `BB_FSR4CAP_ON_HOST` | set: the FSR 4.1.1 capture runs without systemd-run |
| `BB_FSR4CAP_RUNNER` | =steam: the FSR 4.1.1 capture uses Steam's runtime |
| `BB_FSR4CAP_SHELL` | internal to tools/fsr4cap: already inside the nix shell with the tools |
| `BB_FSR4CAP_SPOOF` | =0: the FSR 4.1.1 capture does not present an RDNA4 GPU to AMD's DLL |
| `BB_FSR4CAP_UMU_SHELL` | internal to tools/fsr4cap: already inside the umu nix shell |
| `BB_FSR4_DRIVER_MODELS` | FSR 4 driver models to build (default: navi2 lite) |
| `BB_FSR4_TIERS` | FSR 4 v07 asset tiers to download (tools/fetch_fsr4_assets.sh) |
| `BB_IN_NIX_SHELL` | internal to run.sh: already inside the nix shell |
| `BB_LTO` | =1: link-time optimization (packaging) |
| `BB_PACKAGE_FSR411` | =1: the AppImage carries FSR 4.1.1 models (never in public packages) |
| `BB_PGO` | =generate / use: profile-guided optimization (CMake option) |
| `BB_PGO_DIR` | the PGO profile folder (CMake option) |
| `BB_PUBLISH_BRANCH` | =1: tools/publish_guide.sh publishes the players' guide from a branch other than main |
+73
View File
@@ -0,0 +1,73 @@
# Layer memory module: PS4 GPU addresses without sparse (plan, 2026-10-09)
> Design notes from when this work was planned, kept for their reasoning. bbport 0.7 has one
> memory model and its own renderer, and some switches named here (`BB_PC_MODEL`) no longer exist; the
> current list is in [ENVIRONMENT.md](ENVIRONMENT.md).
## Why
The new memory model keeps game memory in a single sparse arena and moves blocks between game
memory and VRAM by rebinding (`vkQueueBindSparse`). On NVIDIA (Linux, drivers ≥ 555), the bind
time grows with the number of pages already bound, and the call holds a global lock: each
move to VRAM cost seconds, and the game and the desktop froze (GTX 1660 Ti, 615.71). Without moves
(`BB_FIXED_ARENA=1`) it runs at 48.6 FPS versus ~205 on an RX 7800 XT. We need a model that works the
same on AMD, NVIDIA and Intel and is not tied to the shadPS4 buffer cache: part of a future layer
for running PS4 games.
## Idea
The PS4 GPU has its own virtual memory (GPUVM) on top of shared memory; every V#, T# and pointer in
user data is a virtual address. On PC we do the same with Vulkan 1.2 means (BDA):
- **Memory sources**: ordinary (non-sparse) Vulkan buffers:
- chunks of game memory: dma-buf (AMD) or host memory import (NVIDIA, Intel), one chunk per
game direct memory allocation (allocations are mapped whole, so a range inside one
mapping is always contiguous within one chunk);
- VRAM mirrors: segments aligned to game addresses (game page X sits in the segment at the
same offset), allocated when the first VRAM page appears in the segment.
- **Page table** (a buffer on the GPU): page of a PS4 address → device address (BDA) of its
current source. It is changed by a copy in the command stream, in order with draws, without semaphores.
- **Direct path**: the whole V# range is in one source (almost all bindings) → an ordinary
descriptor (source buffer, offset, size). The shader translator does not change.
- **Paged path**: the range is mixed (≈85 huge bindings per frame: shaders bound
to all of memory) → the descriptor points to a record {PS4 address, size}, and the shader
translates each address through the table. A pipeline variant only for such bindings
(specialization key).
- **Vertices, indices, indirect arguments, copies, texture uploads**: the direct path; otherwise,
assembly into a contiguous temporary buffer (read-only) or per-page copies (the CPU knows the table).
- **Moving to VRAM and back**: the policy of the current new model (data whose writes we
know about: loaders, the GPU; unknown writers stay in place), but the execution is a copy + a
table write + choosing the source at bind time. Bindings recorded before a move read the old
source, which is still valid; VRAM is freed once the GPU has passed those commands.
## Order of work
1. **Module core** (`gpu/layer/`, no dependencies on shadPS4): sources, pages, table,
`Resolve(address, size)` → a direct source or "mixed", walking over sources for copies.
The command recording interface is abstract (copy, table write), so it does not pull in the scheduler.
2. **Hook-up without VRAM**: the new model returns ranges in place through chunk buffers, not
through the arena; the new model does not create the sparse arena. Expected: ~48 FPS on AMD (like
`BB_FIXED_ARENA`), no `vkQueueBindSparse` at all. Check the image, then make a build for NVIDIA.
3. **VRAM mirrors + moves**: the policy from `BufferCache`, the page table, the paged path in
shaders for mixed bindings. Goal: parity with the sparse arena on AMD (~205 FPS).
4. **NVIDIA/Intel**: a test build, a tester log; then remove sparse from the new model entirely.
Measurements: RX 7800 XT, standing still (`tools/bench.sh`), `BB_PC_MODEL=1`; diagnostics `BB_RESIDENCY_MIX=1`.
## Status (2026-10-09, morning)
- Steps 1–3 are done. On the RX 7800 XT, standing still: module ~205 FPS, sparse arena ~205–238, old model
~220 (at the lamp in the Hunter's Nightmare); in another spot (1170 draw calls): 167.5 / 164.5 / 146.
`vkQueueBindSparse` is never called in module mode (counter in the `Layer memory:` line).
- Mixed bindings are solved without the paged path: "volatile" blocks (`layer_volatile`). A block with
unknown bytes (the edge of a range, a gap between images) goes into the mirror together with
the rest of the range, is refreshed from game memory before each binding, and whatever a dispatch
wrote into it is copied back after the dispatch (`LayerAfterBindings`). The paged path remains for
bindings of 64 MiB and larger. `BB_LAYER_VOLATILE=0` turns it off (for comparison).
- Default: AMD uses the sparse arena, other GPUs use the module (`BB_LAYER_MEMORY=1/0` to
choose manually). Step 4 (NVIDIA): needs a tester log; the host memory import path cannot be
checked on AMD (RADV does not import memfd).
## Addendum: original copying and portable labels (2026-10-09)
In the new module, the paged path of the original copy shader is fixed: writes update game memory and the current mirror; the native shader is now selected by default. For GPU labels, a standard Vulkan path without AMD buffer marker was added. The implementation, switches and verification limits are described in [TRANSLATION_GPU_COPY_LABELS.ru.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/TRANSLATION_GPU_COPY_LABELS.ru.md). This removes the dependency of label writes on the AMD extension, but does not replace hardware testing on NVIDIA.
+116
View File
@@ -0,0 +1,116 @@
# Mods and third-party patches
Both are in the launcher's **Mods** section and apply at the next start. The game folder itself
is never changed.
## File mods
The **Mods** group has the folder, a **Load mods** switch, and the list of mods: each one can be
turned on or off and moved up or down. The lowest mod that is on wins: when two mods replace the
same file, the log says which one replaced the other.
The mods folder is `~/.local/share/bbport/mods/` with the AppImage (`mods/` in the data
directory when running from the repository). Extract each mod into a folder of its own:
```text
mods/
Mod name/
dvdroot_ps4/
chr/...
param/...
menu/...
```
These layouts work as well:
- `Mod name/app0/dvdroot_ps4/...`, and `Mod name/<title ID>/dvdroot_ps4/...` with whichever
release the mod was made for (`CUSA03173`, `CUSA00900`, ...).
- Up to three extra wrapper folders, as extracting an archive into a folder of its name leaves it:
`Mod name/Mod name v1.2/dvdroot_ps4/...` (notes, pictures and links may sit next to them:
`README`, `.txt`, `.pdf`, `.png`, `.webp`, `.url`, ...).
- The game's folders without `dvdroot_ps4`: `Mod name/chr/...`, `Mod name/parts/...` (`chr`,
`parts`, `map`, `menu`, `msg`, `param`, `sfx`, `sound`, `font` and the other folders of
`dvdroot_ps4`), also next to other folders such as `Screenshots` or `Optional`, which stay out
of the game.
A folder in the mods folder that is not a mod is listed in the launcher under the mods with the
reason: still an archive, several mods in one folder, no game files, or empty.
Upper or lower case does not matter: a mod made on Windows with
`DVDROOT_PS4/Chr/C0000.chrbnd.dcx` replaces the game's `dvdroot_ps4/chr/c0000.chrbnd.dcx`.
The log shows how many game files the mods replaced and added, e.g.
`Mods: 12 game files replaced, 0 added`. A mod meant to replace files that says
`0 game files replaced` has its files in the wrong place.
What works: ready-made replacements of `dvdroot_ps4` files (textures, models, params,
interface, sound, fonts). What does not:
- ZIP or 7z archives: extract them first.
- Two mods changing different parts of the same `.dcx` or `.bnd` file: the last one replaces
it whole, the files are not merged.
- Mods that replace `eboot.bin`, `sce_module` or `sce_sys`, DLL injectors and script plugins.
Changes to the game's code are patches (below).
- Symbolic links inside a mod.
Whether a mod works with Bloodborne 1.09 is up to its author.
A shadPS4-style folder next to the game folder, `<game folder>-mods/dvdroot_ps4/...` (for
example `CUSA03173-mods` next to `CUSA03173`), is loaded too, before the mods in the list. You
can also choose a folder that holds `dvdroot_ps4` directly as the mods folder: it is then loaded
as one mod. **Load mods** turns all of them off. The field of view works the same way, as files
laid under the mods; it is a setting of its own and stays on when mods are off.
### How it works
For each start, the data directory gets a merged tree of links, `out/mod-game-*`: the game sees
the mods' files where they replace its own and the original files everywhere else. `/app0` is
read-only, and saves go to the usual folder. The tree is removed when the game exits normally
or the launcher stops it; one left behind by a crash is not reused.
The order and the mods turned off are kept in `mods.json` in the data directory:
```json
{"order": ["First", "Second"], "disabled": ["First"]}
```
New folders are turned on and added at the end, in alphabetical order.
From a terminal:
```sh
BB_GAME_DIR=/path/to/game BB_MODS_DIR=/path/to/mods bash run.sh
BB_GAME_DIR=/path/to/game BB_MODS_ENABLED=0 bash run.sh
```
`BB_MODS_CONFIG` sets another `mods.json`. `BB_MOD_TRACE=1` (with `BB_MODS_DIR`) logs the
first reads of files that come from mods, to see whether a mod is being used.
## Third-party patches
The **Third-party patches** group loads patches in the shadPS4/GoldHEN XML format from the
patches folder: `~/.local/share/bbport/patches/` with the AppImage, or another folder chosen in
the launcher.
- Only patches for version `01.09` of `eboot.bin` are listed. Files for other games are
skipped: a file applies when it lists any retail release of Bloodborne, or none.
- Each patch starts on or off as the file says (`isEnabled`); the choice in the launcher is
kept in `patches.json` in the data directory:
```json
{"enabled": ["file.xml/Patch name"], "disabled": ["file.xml/Other patch"]}
```
- Lines of type `bytes`, `bytes16`, `bytes32`, `bytes64`, `float32`, `float64`, `utf8` and
`utf16` work. A patch with a `mask` line (a pattern search) or an address outside the
executable is skipped whole, and the log says so.
- They apply after bbport's own patches (`patches/Bloodborne.xml`), so where both change the
same bytes, the third-party patch wins.
- shadPS4's patch file can stay named `Bloodborne.xml`. In a file of that name, the patches
named as one of bbport's own are left out (bbport's apply), and the rest start off until you
turn them on, whatever the file says. An older package's copy of bbport's file adds nothing.
- Patches that replace pointers in tables, such as *60 FPS++*, are moved to where the game is
loaded.
From a terminal: `BB_PATCHES_DIR=/path BB_PATCHES_CONFIG=/path/patches.json bash run.sh`.
This guide is based on bbport's [MODS.md](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/MODS.md) (Russian), by deadinside28.
-106
View File
@@ -1,106 +0,0 @@
# Файловые моды
В лаунчере появилась группа **Моды**: выбор папки, общий переключатель,
включение каждого мода и стрелки порядка загрузки. Изменения применяются при
следующем запуске. Нижний включённый мод имеет больший приоритет; при совпадении
файлов в журнале выводится, какой мод заменил предыдущий.
По умолчанию используется `bbport/mods/`, а в AppImage —
`~/.local/share/bbport/mods/`. Распакуйте скачанный мод так:
```text
mods/
Название мода/
dvdroot_ps4/
chr/...
param/...
menu/...
```
Также принимаются:
- `Название мода/app0/dvdroot_ps4/...` и `Название мода/CUSA03173/dvdroot_ps4/...`;
- лишняя папка-обёртка, как после распаковки архива в папку с его именем:
`Название мода/Название мода v1.2/dvdroot_ps4/...` (рядом могут лежать readme/картинки);
- мод без `dvdroot_ps4`, в котором сразу лежат папки игры: `Название мода/chr/...`,
`Название мода/parts/...` (`chr`, `parts`, `map`, `menu`, `msg`, `param`, `sfx`,
`sound`, `font` и другие папки `dvdroot_ps4`).
Регистр имён в моде не важен: `DVDROOT_PS4/Chr/C0000.chrbnd.dcx` из мода, собранного
под Windows, заменяет игровой `dvdroot_ps4/chr/c0000.chrbnd.dcx`. Раньше такой файл
добавлялся рядом под своим именем, и игра его не видела. В журнале запуска видно, сколько
файлов игры мод заменил и сколько добавил: `Mods: 12 game files replaced, 0 added`.
Если мод должен заменять файлы, а пишет `0 game files replaced`, проверьте пути внутри мода.
Архивы ZIP/7z нужно предварительно распаковать. Поддерживаются готовые замены
файлов `dvdroot_ps4`: текстуры, модели, параметры, интерфейс, звук и шрифты.
Менеджер не объединяет содержимое двух одинаковых `.dcx`/`.bnd` файлов: последняя
замена выигрывает целиком.
Для уже подготовленной папки shadPS4 работает соседний каталог
`CUSA03173-mods/dvdroot_ps4/...` рядом с `CUSA03173/`. Он подключается автоматически
перед модами из списка. Общий переключатель выключает и его. Можно также выбрать
папку, непосредственно содержащую `dvdroot_ps4`, как папку модов; такая папка
подключается целиком, без списка отдельных модов.
Соглашение `-mods` проверено по
[исходникам shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/file_sys/fs.cpp).
Оригиналы игры не перезаписываются. На время запуска создаётся отдельное
объединённое дерево ссылок в каталоге данных `out/mod-game-*`; чтение, stat и
перечисление каталогов видят выбранные замены и остальные оригинальные файлы.
Игровые `/app0` и `/hostapp` доступны только для чтения. Сохранения остаются
в прежней папке и доступны для записи. При обычном завершении или остановке
лаунчером временное дерево удаляется. После SIGKILL/аварийного отключения оно
может остаться; такие каталоги не используются в следующем запуске.
Моды с заменой `eboot.bin`, `sce_module` или `sce_sys`, инжекторы DLL и скриптовые
плагины этим загрузчиком не поддерживаются. Патчи исполняемого кода остаются
в существующей системе `patches/` / `BB_PATCHES`. Символьные ссылки внутри мода
не принимаются. Совместимость конкретного мода с CUSA03173 1.09 определяется
его автором.
Для запуска из терминала:
```sh
BB_GAME_DIR=/path/to/CUSA03173 BB_MODS_DIR=/path/to/mods bash run.sh
BB_GAME_DIR=/path/to/CUSA03173 BB_MODS_ENABLED=0 bash run.sh
```
Порядок и отключения сохраняются в каталоге данных `mods.json`:
```json
{"order": ["Первый", "Второй"], "disabled": ["Первый"]}
```
Новые папки включаются автоматически и добавляются в конец по алфавиту.
`BB_MODS_CONFIG` меняет путь профиля. `BB_MOD_TRACE=1` с `BB_MODS_DIR` выводит
первые чтения реально подключённых файлов для диагностики.
# Сторонние патчи (XML shadPS4)
Папка `patches/` в каталоге данных (в AppImage — `~/.local/share/bbport/patches/`, в лаунчере
группа **Сторонние патчи**, там же можно выбрать другую папку). Подходят XML-файлы патчей в
формате shadPS4/GoldHEN: берутся `Metadata` для `AppVer="01.09"` и `eboot.bin`, файлы с
`TitleID` других игр пропускаются. Включение по умолчанию — из `isEnabled` файла, выбор в
лаунчере сохраняется в `patches.json` каталога данных:
```json
{"enabled": ["файл.xml/Имя патча"], "disabled": ["файл.xml/Другой патч"]}
```
Поддерживаются строки `bytes`, `bytes16/32/64`, `float32/64`, `utf8`, `utf16`.
Патч со строками `mask` (поиск по шаблону) или с адресом вне eboot пропускается целиком,
с сообщением в журнале. Сторонние патчи применяются после встроенных
(`patches/Bloodborne.xml`): при пересечении побеждает сторонний. Из терминала:
`BB_PATCHES_DIR=/путь BB_PATCHES_CONFIG=/путь/patches.json bash run.sh`.
Патчи, которые заменяют указатели в таблицах (как `60 FPS++` и `90 FPS++`), загрузчик
переносит на реальный адрес образа. Раньше такие патчи останавливали запуск ошибкой
`patch overlaps a relocation`.
# Резкость TAA
Настройки `sharpen` и `sharpness` теперь работают также для TAA, в лаунчере и
меню игры. Диапазон — **0…1**; при 0 или выключенном переключателе дополнительного
прохода нет. RCAS применяется к готовой сцене перед HUD. В историю TAA сохраняется
цвет до повышения резкости, чтобы она не усиливалась при повторном накоплении.
Ровные участки сохраняют исходный цвет, включая HDR; дополнительный буфер не нужен.
+151
View File
@@ -0,0 +1,151 @@
# Online play and co-op
Online play is a beta. Messages, bloodstains and ghosts come from a community game server:
Seamless Hunt's (the default) or [The Hunter's Dream](https://thehuntersdream.com). Bells and summons go through a co-op pool,
and then the players' games connect to each other. Linux and Windows players can play together:
everyone needs the game at version 1.09 and the same pool.
> **shadPS4's public shadNet server no longer accepts bbport builds**: it signs the game out
> right after the login ("The shadNet server doesn't accept this shadPS4 build" in the log), and
> the launcher says so. The pools for bbport are Seamless Hunt (the default), The Hunter's Dream's and a server of your own.
## Seamless Hunt
[Seamless Hunt](https://install.seamlesshunt.com) is a community server for Seamless Co-op (BBSC),
where Windows players join with BBSC's package for the Windows port.
1. In the launcher's **Multiplayer → Online** tab, turn on **Play online** and choose **Seamless Hunt**: its
shadNet server, WebAPI and game server come with it.
2. Type a name and a password under **Account** and press **Create** (once). The server wants an
email for each account; the launcher makes one from the name, as Seamless Hunt's own tool does.
3. Turn on BBSC in the **Seamless co-op (BBSC)** group and press **Test** in the **Status** row.
Its bloodstains and death visions crash the game with "Transaction failed", on Windows too: they
are off by default, as in Seamless Hunt's package (**Bloodstains on Seamless Hunt** brings them
back). Messages, ghosts and summon signs are not affected.
## The Hunter's Dream (with bbhost's players)
Choose **The Hunter's Dream (with bbhost players)** in the launcher's **Multiplayer → Online** tab.
The game's online module talks to The Hunter's Dream itself, the way bbhost does (from bbport 0.7):
nothing else runs next to the game. Its account is your Discord sign-in, the same one bbhost uses:
1. Press **Sign in with Discord**. The launcher shows a code, copies it and opens The Hunter's
Dream's page.
2. On that page, press *Sign in with Discord to link* and sign in; back on the page, enter the code
in the code field (not in *Sign in with a code from the game*).
No password is kept: the launcher keeps the sign-in's token with its settings. **Sign out** forgets
it. **Test** checks The Hunter's Dream's game server and its co-op service. Seamless co-op (BBSC)
is not available there.
Report problems here ([issues](https://github.com/ayozetr/bloodborne_linux/issues)), not to The
Hunter's Dream's support.
## Your own server
Its owner creates the account ([hosting your own server](SERVER.md)), or you make it with **Create** under
**Account** when the server lets anyone register.
1. In the launcher's **Multiplayer → Online** tab, turn on **Play online**, choose **Your own server** with
its address, and enter the **account name** (not the email) and the password. The password is
kept on this computer with the launcher's settings.
2. Press **Test** in the **Status** row: the game server, the shadNet server, its UDP port and its WebAPI
should all answer, and with the account filled in, the sign-in should work. A wrong name or
password shows there (the server does not say which of the two).
If the game says you are not signed in to PlayStation Network, the shadNet login failed: the log
then has a shadNet login error (code 8 is a wrong name or password). If the server did not answer
when the game started, the game keeps trying in the background.
## Seamless co-op (BBSC)
[BBSC](https://github.com/Classmg22/BBSC) (Bloodborne Seamless Co-op, by CLS_AMG) keeps a co-op
session going through deaths, lamps, areas, bosses and the Hunter's Dream, for up to 5 hunters:
the guest follows the host through lamps, sees the host's world (doors, bosses, lifts), keeps the
items and echoes it gets, and enemies get stronger for every guest. It runs inside the game's
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, **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,
five hunters, enemy scaling and its percentages per guest, shared progress and the rest; only
the ones changed from BBSC's defaults are passed to the game.
2. Every player of the session needs BBSC, the same version: on Linux this port, on Windows
BBSC's package for the Windows port (its *BBport* release). A player without it plays the
normal game.
3. The server decides how much of it works:
- **BBSC's game server** (Seamless Hunt's, which comes with it, or your group's,
[hosted with it](SERVER.md#seamless-co-op-bbsc)): everything, summons across areas and 5 hunters included. Set it as the
**Game server** of the Online tab.
- **The Hunter's Dream** has no BBSC: the normal game's co-op, with bbhost's players.
With BBSC and BBSC's game server, two players who cannot reach each other directly (CGNAT,
strict routers, two games on one PC) are connected through that server's UDP relay (port
3478); `SHADPS4_P2P_RELAY=0` turns it off.
BBSC gives every character its own items, which cost no Insight: the **Hunt Bell** (call other
players), the **Kin Bell** (join another player), the **Bloodlust Bell** (invade), the **Duel Bell**
(PvP on and off during co-op) and the **Parting Blank** (end co-op). It takes the game's Beckoning
Bell, Small Resonant Bell and Silencing Blank out of the inventory, and they do not come back when
BBSC is turned off; BBSC's own items stay, with their names (scripts/bbsc_kit_texts.py), and work again
when it is back on. Before changing anything it backs the saves up to `user/bbsc/save-backups/`:
copy a character's save from there to play it without BBSC with its old bells. Its texts follow the
language the game shows its text in (*Game text language* in the launcher, else the system's):
Spanish, French, German, Italian, Portuguese and Russian; English otherwise. **F8** puts you back
on the ground if you get stuck inside another player.
**Not yet tested with more than one player on Linux:** one player alone works (its items, deaths,
lamps, the Hunter's Dream). Report what you find with the log.
Its log is in `user/bbsc/logs/` (in the AppImage, `~/.local/share/bbport/user/bbsc/logs/`), and
the game's log has its warnings (`BBSC:` lines). Not in the Linux build: BBSC's cinematic camera
(L3 + R3 opens bbport's menu) and its own Discord status (bbport has one).
## Summoning in the game
- The host rings the **Beckoning Bell** (one Insight each time); the helper rings the **Small
Resonant Bell**. With BBSC: the **Hunt Bell** and the **Kin Bell**, at no Insight cost.
- Use the same **password** in the game's network settings, so you find each other whatever
your levels.
- Be in the same area, close to each other, with the area's boss still alive in the host's
world.
## Ports
Co-op traffic is UDP, straight between the players' computers. With your own server on
Tailscale ([hosting your own server](SERVER.md)) it goes through Tailscale: turn **UPnP** off and skip this
section. Otherwise each player has to be reachable from the Internet:
1. **The router.** With **UPnP** on (the default), the game opens its port on the router itself:
the log says `P2P on UDP port <port>: asking the router to forward it (UPnP)`. A router
without UPnP needs a port forward by hand (UDP, the port below, to your computer's LAN
address).
2. **The port** to open by hand or in a firewall is UDP 3658, the PS4's own, which the game uses
by default (**Co-op port (UDP)** in the Online tab: 0 keeps it). The log says
`P2P transport on UDP port 3658`.
3. **Open it in the firewall.** UPnP opens the port on the router only, not in this computer's
firewall. With ufw or firewalld on, the launcher's **Multiplayer → Online** tab says
so and gives the commands. For ufw:
```sh
sudo ufw allow 3658/udp
sudo ufw allow from <router IP> proto udp # the router's UPnP answers
```
Windows players open the same UDP port in their own firewall.
A router whose public address is shared by many customers (CG-NAT) cannot forward ports: the
log's `UPNP: IGD found - external IP=...` should be your real public address. Tailscale works
behind CG-NAT too.
## Reading the log
Run with `BB_GPU_LOG=info` in the extra variables for the online details. In a summon:
| Line | Meaning |
|---|---|
| `signed in npid='...'` | The shadNet login worked. |
| `summon_messenger/create` | Your sign is placed (helper). |
| `summon_messenger/get`, `sceNpMatching2CreateJoinRoom`, `summon_messenger/request` | The host found the sign, made the room and sent the summon. |
| `signed out by the server` | The server refuses this build (shadPS4's public one): use Seamless Hunt, The Hunter's Dream or your own server. |
+39
View File
@@ -0,0 +1,39 @@
# Documentation
## For players
The same documents, with a few pages written for players, make up the
[players' guide](https://ayozetr.github.io/bloodborne_linux/) (built from this folder: `mkdocs.yml`; published with `tools/publish_guide.sh`).
- [getting-started.md](getting-started.md): what you need, the first launch, saves and updates.
- [steam-deck.md](steam-deck.md): Add to Steam, the Steam Deck's Game Mode, Steam Input and the overlay.
- [logs.md](logs.md): getting a log, also from Game Mode, and sending it.
- [faq.md](faq.md): the questions players ask most.
- [COMPATIBILITY.md](COMPATIBILITY.md): which releases of Bloodborne run, and which have been
checked.
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md): common problems and what to do.
- [ONLINE.md](ONLINE.md): online play and co-op: the account, the bells, the ports.
- [SERVER.md](SERVER.md): hosting your own co-op server for a group of friends.
- [MODS.en.md](MODS.en.md): file mods and third-party patches.
## For developers
- [ENVIRONMENT.md](ENVIRONMENT.md): every environment variable the port reads, with its default
and where in the code it is read.
- [DEBUGGING.md](DEBUGGING.md): catching a crash with a debugger (`BB_FAULT_WAIT`).
- [RELEASE_TEMPLATE.md](RELEASE_TEMPLATE.md): the format of every release's notes.
- [CREDITS.md](CREDITS.md): whose work is in the port, and its licenses.
## Design notes from upstream bbport
The port is built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28. These
notes explain parts of its engine that this port still runs, and the code points to them. Some
describe plans or measurements at the date in their title.
- **GPU commands in parallel:** [parallel_gpu.md](parallel_gpu.md).
- **Upscaling, TAA and frame generation:** [upscaler.md](upscaler.md).
- **The translator that replaces emulation:** [TRANSLATOR_ARCHITECTURE.md](TRANSLATOR_ARCHITECTURE.md).
- **The GPU layer's memory module:** [MEMORY_MODULE_PLAN.md](MEMORY_MODULE_PLAN.md).
Upstream's release notes, roadmap, session logs and its other notes (most in Russian) stay in
[its docs folder](https://github.com/deadinside28/bloodborne_pc/tree/master/docs).
+4 -2
View File
@@ -11,8 +11,9 @@ changes since the previous release. Third-party work is credited in the line tha
```markdown
<One or two sentences: what this release is and which upstream bbport it is based on.>
**Requirements:** your own decrypted dump of Bloodborne 1.09, US (`CUSA03173`) or EU
(`CUSA00900`). No game files are included. Linux x86-64 and a Vulkan 1.3 GPU.
**Requirements:** your own decrypted dump of Bloodborne with update 1.09, any retail release (US,
Europe, UK, Japan, Asia, and the editions with The Old Hunters). No game files are included.
Linux x86-64 and a Vulkan 1.3 GPU.
## Download
@@ -42,4 +43,5 @@ SHA-256: `<sha256sum of the AppImage>`
**Full changes:** https://github.com/ayozetr/bloodborne_linux/compare/<previous tag>...<tag>
**Credits and licenses:** https://github.com/ayozetr/bloodborne_linux#credits
Includes Lantern Engine by Awaken (https://github.com/Awakenchan/LanternEngine).
```
-206
View File
@@ -1,206 +0,0 @@
# Нативный порт Bloodborne: что сделано и что дальше
Основной текст — состояние на 2026-09-26; ниже первым разделом — что изменилось к 30.09.
Самый подробный срез: [CURRENT_STATUS_2026-09-29.md](history/CURRENT_STATUS_2026-09-29.md).
Отчёт о работе 30.09–01.10 (конвейер GPU, апскейлер, падение, лаунчер, AppImage):
[SESSION_2026-09-30.md](history/SESSION_2026-09-30.md).
Подробности по отдельным темам:
[parallel_gpu.md](parallel_gpu.md) (многопоточность GPU, подтормаживания при подгрузке),
[upscaler.md](upscaler.md) (анализ кадра игры, константы сцены).
## Обновление 30.09
- Сделано после 26.09: FSR 4 INT8 (v07), векторы движения объектов, смена пресета без
перезапуска (уменьшенные цели сцены; глубина D32S8 копируется шейдером), LTO и PGO,
масштабирование подготовки draw под число потоков (Steam Deck тоже).
- Проблемы из раздела «Известные проблемы» ниже: пункт 3 (перезапуск при смене пресета)
решён; пункт 1 (векторы объектов) реализован, у FSR 4 остаётся гостинг на персонаже при
Ultra Performance; пункт 2 (мыльный интерфейс) — UI рисуется в 1920×1080.
- Ограничение: уменьшенный рендер сцены (`SceneTargets::Eligible`) требует цель 1920×1080,
то есть работает только при выводе 1080p.
- Распараллеливание потока GPU-команд (шаг 2 [parallel_gpu.md](parallel_gpu.md)): текстуры на
отдельном потоке параллельно с буферами работают корректно, но прироста не дают —
данные каждого draw переезжают между ядрами. Узкое место — поток GPU-команд: 91% на CPU,
~9% ожидания мелких копий гостевой памяти перед fence. Дальше — двухстадийный конвейер
(второй поток владеет текстурным кешем и записью draw, копия регистров из дельт сканера)
или мемоизация привязок текстур между кадрами; см. «What would scale» в parallel_gpu.md.
- **Двухстадийный конвейер draw** (`vk_draw_pipe.h`): поток GPU-команд декодирует PM4 и
выбирает пайплайн, второй поток (`bb:DrawRec`) со своей копией регистров привязывает ресурсы
и записывает draw. +19% FPS на 16 потоках, +18% на 8 (как Steam Deck). Включён по умолчанию
при 8+ потоках (`BB_DRAW_PIPE=0/1`). Узкое место теперь — вторая стадия; подробности и план —
в parallel_gpu.md.
- Пресеты и нагрузка GPU: сцена на RX 7800 XT стоит мало, поэтому пресет меняет время GPU
лишь на ~0.5 мс, а сам FSR 4 при выводе 1080p — ~1.4 мс (FSR 4 Ultra Performance дороже
нативного рендера без апскейлера). Исправлено: проходы освещения оставались в 1920×1080 при
любом пресете (проверка смотрела на чужие слоты целей); уменьшенные цели теперь сэмплируются
напрямую, без ресэмплинга в натив (кэш шейдеров пересобирается один раз). Следующее для
Steam Deck — гостевая копия памяти целей рендеринга `3d5ebf4e` (см. parallel_gpu.md).
- **Исправлено падение с конвейером** (порча гостевой кучи, guest offset `0x263b8e7`, через
13–17 минут): прерывание «GPU простаивает» уходило раньше меток EOP, отложенных в поток записи
Vulkan, и игра освобождала объекты с этими метками. Теперь перед ним конвейер и отложенные
метки дожидаются; 2 прогона по 30 минут без падений (до исправления — 7 из 7 падений).
- **Разрешение вывода 1440p/2160p** (меню «Разрешение вывода», bbport.ini `output_res`, после
перезапуска): сцена рисуется в разрешении пресета от выхода, апскейлер (FSR 3 или FSR 4)
дорисовывает кадр, интерфейс растеризуется в выходном разрешении. Эффекты игры (аберрация,
DoF, размытие, SSAO, сглаживание игры, динамические тени, SSR, заставки, LOD) — там же.
- **Лаунчер** (`launcher/bb-launcher.sh`, GTK4/libadwaita) и **AppImage** для Steam Deck
(`bash packaging/appimage.sh` → `dist/`; данные в `~/.local/share/bbport`, `--play` —
запуск без окна лаунчера).
- Инструменты: `BB_PAD_FILE` (скриптовый ввод геймпада), `ab.sh` (A/B битов
`BB_TOGGLE_FILE` в запущенной игре), в `Frame stats` — простой и блокировки потока GPU.
## Как устроен порт
- Оригинальный eboot (CUSA03173 v01.09) исполняется напрямую на x86-64. Системные библиотеки
PS4 заменены своим HLE-рантаймом на C (`runtime_*.c`, `probe.c`).
- Графика: видеоядро и рекомпилятор шейдеров shadPS4 (GPL), собранные в `libbbgpu.so`
(`gpu/`), с собственными доработками. Эмулятор shadPS4 целиком не используется.
- Патчи игры (FPS, разрешение и др.) — XML сообщества (`patches/Bloodborne.xml`),
компилируются `patches.py` и накладываются загрузчиком при старте.
## Сделано
### Производительность и плавность
| Что | Результат |
|---|---|
| Запись команд Vulkan в отдельном потоке, кеши текстур/буферов, rwlock вместо мьютекса | 26 → 64 FPS в одной сцене |
| Подготовка draw-вызовов на 4 потоках (`bb:DrawPrep`), проверка на потоке GPU | +11.5% |
| Окно снятия защиты 256 КиБ при записи игры в память (меньше page fault) | +22% |
| Кеш описаний текстур, быстрый путь для тех же render target | до ~94 FPS на улице |
| Копирования гостевой памяти на пуле потоков, пул staging-буферов | фризы при подгрузке 60–170 мс → 40–50 мс |
| Ожидание копий перед видимыми игре fence | убрано мерцание интерфейса |
| Режим «Readbacks Relaxed» | нет «взрывов вершин» (FaceGen) |
| Vblank 480 Гц + лимит кадров до min(монитор, 120), вывод сразу по флипу | ровная подача кадров; бег не замедляется выше 120 FPS |
Всё переключается во время игры через `BB_TOGGLE_FILE` (битовая маска) для A/B-сравнений.
Статистика: `BB_FRAME_STATS=1`.
### Звук, ввод
- Вывод звука с правильным темпом (раньше щелчки каждые 10.7 мс).
- Геймпад через SDL3, запасная раскладка клавиатуры.
### Анализ кадра
- `BB_CAPTURE_TRIGGER=<файл>` записывает один кадр: проходы, цели, шейдеры, текстуры,
константы, режимы смешивания.
- Разобрано устройство кадра Bloodborne (G-buffer, свет, цвет сцены RGBA16F, туман,
пост-обработка, тонмаппинг, сглаживание игры, интерфейс) и 864-байтные константы сцены
(матрицы вида и проекции).
### Временной апскейлер (FSR 3.1)
- **Векторы движения камеры**: из глубины сцены и матриц текущего и прошлого кадра
(`vk_camera_motion.cpp`, `camera_motion.comp`). Проверены перепроецированием прошлого
кадра: статичная геометрия совпадает.
- **Jitter**: субпиксельный сдвиг геометрии сцены через viewport, последовательность
Halton(2,3); число фаз 8·(вывод/рендер)². Интерфейс и пост-обработка не сдвигаются.
- **FSR 3.1** (FireBurn/FSR-Vulkan, нативный Vulkan, подмодуль `gpu/third_party/fsr-vulkan`):
- *Native AA*: на HDR-цвете сцены перед пост-обработкой, 1:1.
- *Пресеты Quality/Balanced/Performance/Ultra Performance* (x1.5/1.7/2/3): игра рендерит в
пониженном разрешении (патч разрешения, который `patches.py` выводит из патча 720p
сообщества), FSR увеличивает готовый кадр до 1920x1080 перед первым проходом интерфейса.
Интерфейс и проход вывода перенаправлены в изображения полного размера, презентер
показывает их вместо буфера игры.
- **Маска реактивности** (снимок сцены до прозрачных отрисовок, сравнение перед наложением
тумана): работает, но на дымке Bloodborne даёт дрожание — по умолчанию выключена.
- **Внутриигровое меню** (Dear ImGui, Insert или L3+R3): апскейлер, пресет, резкость,
jitter, маска, счётчик FPS. Настройки в `bbport.ini`. Смена пресета — кнопка
«Применить и перезапустить игру».
## Известные проблемы
Состояние последнего коммита. В рабочем дереве подготовлен перенос пресетов на уменьшенные
цели рендера при сохранении гостевых буферов, постобработки и UI в 1920×1080. Переходы
между пресетами и копирование цвета/глубины/stencil прошли Vulkan-тест на Lavapipe.
RX 7800 XT не поддерживает blit D32S8, поэтому новый путь оставлял FSR вход 1920×1080
и снижал FPS. Для таких GPU запуск теперь автоматически выбирает проверенный путь с патчем
разрешения игры и UI 1920×1080; смена пресета снова требует перезапуска. На GPU с нужной
поддержкой остаётся переключение без перезапуска. Пункты 2–3 ниже относятся к последнему
коммиту, до этих изменений рабочего дерева.
Общий проход векторов движения объектов был слишком дорогим: Performance давал около
49 FPS с ним и 77–78 FPS без него. Теперь дополнительная цель и шейдеры включаются только
для G-buffer отрисовок с буфером позы/костей; остальные используют векторы камеры.
В пробном запуске выборочный путь держал около 81–87 FPS, без пропусков из-за ёмкости
истории. Векторы объектов включены по умолчанию; `BB_OBJECT_MOTION=0` отключает их, а
`BB_OBJECT_MOTION_ALL=1` возвращает старый общий путь для сравнения. Выбор по размеру
буфера пока эвристический: качество оружия и одежды в движении требует проверки в игре.
1. **Движущиеся объекты «рассыпаются»** (оружие на спине, персонаж, ткань, враги): есть
только векторы движения камеры. Камера от третьего лица следует за персонажем, поэтому
векторы камеры для него неверны, и апскейлер берёт историю не из того места. Это касается
любого временного апскейлера (FSR 3/4, DLSS, XeSS).
2. **Интерфейс в пресетах мыльный**: с патчем разрешения игра считает себя 540p-игрой и,
судя по всему, готовит шрифты и элементы интерфейса под это разрешение. Меню
(титульное, пауза) вообще не проходят через апскейлер.
3. **Смена пресета требует перезапуска**: разрешение задаётся патчем кода при старте, а
буферы игра выделяет один раз.
## План
### 1. Векторы движения объектов
Нужны всем апскейлерам и генерации кадров. Игра их не считает, поэтому считаем сами.
Как:
- Для каждой отрисовки в G-buffer (проходы с 5+ целями) рекомпилятор делает второй вариант
вершинного шейдера, который выдаёт позицию дважды: с текущими константами и с константами
прошлого кадра. Разница проекций — вектор движения, пишется во вспомогательную цель
RG16F как дополнительный выход пиксельного шейдера.
- Константы прошлого кадра: для каждого draw запоминаем содержимое константных буферов
(матрица объекта, кости скелета) по ключу «шейдер + номер draw в проходе» или по адресу
данных. Нужно выяснить, как игра хранит матрицы скелета: в буферах, которые перезаписываются
каждый кадр (тогда копируем прошлую версию), или в кольцевом буфере (тогда достаточно
адреса прошлого кадра).
- Где объект не найден (первый кадр, новый объект) — векторы камеры, как сейчас.
- Проверка: отладочный вид векторов (`BB_DEBUG_MOTION`), перепроецирование прошлого кадра
по полным векторам, визуально — пила на спине при беге.
### 2. Масштабирование на уровне рендерера (вместо патча разрешения)
Игра работает в 1920x1080 (или в разрешении монитора), а в пониженном разрешении рисуются
только проходы 3D-сцены. Интерфейс и меню остаются чёткими, пресет меняется на лету.
Как:
- Кеш текстур выделяет для целей сцены (G-buffer, глубина, свет, цвет сцены, эффекты) копию
уменьшенного размера; гостевая память и описания игры не меняются.
- Viewport и scissor проходов в эти цели масштабируются (как уже сделано для интерфейса,
включая проходы с отключённым отсечением).
- Шейдеры, которые считают по пиксельным координатам (`gl_FragCoord`, `texelFetch`,
compute-шейдеры по пикселям), получают коэффициент масштаба через push-константы
рекомпилятора. Выборки по нормированным координатам работают без изменений.
- FSR ставится туда же, где Native AA: цвет сцены перед пост-обработкой (HDR, лучшее
качество); пост-обработка и интерфейс идут в полном разрешении.
- Риски: проходы, читающие цели сцены по пиксельным координатам в пост-обработке, и
чтения из гостевой памяти этих целей (их нужно запретить или масштабировать обратно).
### 3. Другие апскейлеры
Все используют одни и те же входы (цвет, глубина, векторы движения, jitter, маски).
- **FSR 4 (INT8)**: в FSR-Vulkan есть экспериментальный провайдер FSR 4 v07 INT8 — добавить
как вариант в меню и сравнить с FSR 3.1 после шага 1.
- **DLSS**: нативный Linux Vulkan SDK (NGX). На RX 7800 XT не работает, но нужен для
пользователей NVIDIA.
- **XeSS / XeFG и OptiScaler**: Intel поставляет их только как Windows DLL. Варианты:
загрузчик Windows DLL (PE) внутри процесса либо запуск всей сборки под Proton с
OptiScaler, которому отдаём входы через интерфейс DLSS.
Проверено 29.09: libxess.dll (одинаковая у DOOM: The Dark Ages, PRAGMATA и др.) имеет
Vulkan-путь (`xessVKCreateContext`, `xessVKExecute`), но импортирует ~300 функций:
MSVCP140 (iostream), VCRUNTIME140 (C++-исключения), UCRT, kernel32, setupapi, dxgi.
Решено отложить; самый реалистичный путь — хелпер под Wine с общей памятью Vulkan.
### 4. Генерация кадров
- FSR 3.1 Frame Generation (есть в FSR-Vulkan): промежуточный кадр между двумя готовыми,
по тем же векторам движения; интерфейс нужно рисовать поверх отдельно (есть перенаправление
интерфейса — его можно рендерить в отдельный слой).
- XeFG — после решения с Windows DLL (см. выше).
- Требование: подача кадров через презентер (очередь и VRR уже работают).
### 5. Прочее
- Steam Deck: портативная сборка (patchelf, без Nix), проверка пресетов на 1280x800.
- Дальнейшая разгрузка потока GPU (шаг 2 из [parallel_gpu.md](parallel_gpu.md)).
+206
View File
@@ -0,0 +1,206 @@
# Hosting your own co-op server
Bells and summons go through a shadNet server. shadPS4's public one (`srv.shadps4.net`) no longer
accepts bbport builds, and the public pools for bbport are Seamless Hunt (the default) and The
Hunter's Dream's (the launcher's **Multiplayer → Online** tab; [README](../README.md#online-play)). A group of friends
can also run their own server:
[shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) by Wozzardman, the shadNet server with
that UDP port working and a summon service of its own.
With your own shadNet server, messages, bloodstains and ghosts come from the **Game server** field
of the Online tab: The Hunter's Dream by default, or BBSC's game server ([below](#seamless-co-op-bbsc)). Windows players of [Supermedo's port](https://github.com/Supermedo/bloodborne_pc) can join
the same server.
Tested with two players, one on this Linux port and one on Supermedo's Windows port, over
Tailscale, with the server below on a home server (Ubuntu 26.04): summons, a boss fight together
and summoning again after the helper died.
## How it fits together
- **One always-on computer runs the server:** a VPS, a home server, or one of the players' PCs
(then it has to be on while you play).
- **Everyone joins one [Tailscale](https://tailscale.com) network** (a *tailnet*): the server and
every player. Tailscale gives each computer an address starting with `100.` and encrypts the
traffic between them. This matters: shadNet's login on TCP 31313 is not encrypted, so it should
not be open to the Internet. And the games then reach each other over Tailscale too, so nobody
has to open ports on their router.
| Protocol | Port | Used for |
|---|---|---|
| TCP | 31313 | Login and matchmaking |
| UDP | 31314 | Each player's address, for the other player of a summon |
| TCP | 31315 | WebAPI and the summon service |
## 1. Tailscale
On the server and on every player's computer, install Tailscale
([download](https://tailscale.com/download); on Linux `curl -fsSL https://tailscale.com/install.sh | sh`)
and sign in:
```sh
sudo tailscale up
```
The server's owner invites the players from the
[admin console](https://login.tailscale.com/admin/users) (**Users → Invite external users**),
and each player accepts the invitation and signs in on the computer they play on. Then, on the
server:
```sh
tailscale ip -4 # the server's address, 100.x.y.z: the players need it
tailscale status # every player's computer should be listed
```
## 2. The server
### With Docker (any Linux)
This repository has a Docker image for it: [tools/shadnet-server](../tools/shadnet-server). It
builds shadnet-p2p on Debian 13, because shadnet-p2p needs Qt 6.8 or newer and some
distributions are older (Ubuntu 24.04 has Qt 6.4). It runs on any Linux with Docker, Ubuntu 24.04
included.
```sh
mkdir shadnet-server && cd shadnet-server
for f in Dockerfile entrypoint.sh; do
curl -fsSLO "https://raw.githubusercontent.com/ayozetr/bloodborne_linux/main/tools/shadnet-server/$f"
done
docker build -t bbport-shadnet .
```
Then start it, with the server's Tailscale address and a signup code of your own (letters,
digits, `-` and `_`; the players never need it, only you, to create their accounts):
```sh
docker run -d --name shadnet --restart unless-stopped --network host \
-v shadnet-data:/data \
-e SHADNET_HOST=100.x.y.z \
-e SHADNET_REGISTRATION_KEY=a-long-code-of-your-own \
bbport-shadnet
docker logs shadnet
```
The log should end with the three listeners on the Tailscale address:
```text
Unsecured TCP listener on: 100.x.y.z:31313. No encryption, use it only on trusted networks
STUN UDP listener on: 100.x.y.z:31314
WebApiServer listening on: 100.x.y.z:31315
```
`SHADNET_HOST` keeps the server off the computer's other networks. After a reboot, it waits until
Tailscale is up. The configuration (`shadnet.cfg`) and the accounts (`db/shadnet.db`) live in the
`shadnet-data` volume. To stop or update the server:
```sh
docker stop shadnet && docker rm shadnet # the volume, with the accounts, stays
docker build -t bbport-shadnet . # after downloading a newer Dockerfile
```
Then run the same `docker run` again.
### Without Docker
Follow [shadnet-p2p's README](https://github.com/Wozzardman/shadnet-p2p#readme). It needs Qt 6.8
or newer: Arch Linux and its derivatives, Debian 13, Ubuntu 26.04, Fedora 41 or newer. On Debian
and Ubuntu, install `qt6-websockets-dev` along with the packages it lists, or CMake does not find
Qt's HTTP server module (checked on Debian 13 and Ubuntu 26.04). In `build/shadnet.cfg`, set `Host` to the server's Tailscale address (or
`0.0.0.0`), `Matching2Enabled=true` and `RegistrationSecretKey` to your signup code.
### A firewall on the server
With ufw, let the tailnet in:
```sh
sudo ufw allow in on tailscale0
```
## 3. Accounts
One account per player, created on the server with the signup code. The name is 3 to 16
letters, digits, `-` or `_`:
```sh
docker exec shadnet shadnet-sample 127.0.0.1 31313 register <name> <password> <email> <signup code>
```
`127.0.0.1` works only when the server listens on every address (`SHADNET_HOST` unset or
`0.0.0.0`). With `SHADNET_HOST` set, use that address instead. The tool answers
`Account created successfully.`, or `FAILED: Unauthorized` when the code is wrong. Give each player
their own name and password.
To back up the accounts, stop the server and copy the volume's `db/shadnet.db`.
## 4. Each player's launcher
In **Multiplayer → Online**:
| Setting | Value |
|---|---|
| Play online | On |
| shadNet server | **Your own server** |
| Server address | `100.x.y.z` (the port, 31313, is added) |
| WebAPI address | Empty (the server's port 31315) |
| Online ID / Password | The player's own account |
| UPnP | Off (Tailscale needs no router ports) |
Then press **Test** in the **Status** row. Every line should get a check mark, the shadNet server's UDP among them: it
shows the address the server sees for you, a `100.` one through Tailscale. An error mark on the UDP line
means summons cannot connect: check that the server is running and that its log lists the UDP listener.
Summons go through the server's own summon service: the log of a game says
`Co-op: summon signs through http://100.x.y.z:31315`.
Windows players set the same server, WebAPI and account in the Windows port's launcher.
## Seamless co-op (BBSC)
For [BBSC](ONLINE.md#seamless-co-op-bbsc) with everything on (summons across areas, 5 hunters),
the group also runs BBSC's game server next to shadNet. It is in this repository,
`scripts/bbsc_gameserver/bbsc_gameserver.py` (Python 3, nothing to install), and it takes the
place of The Hunter's Dream for messages, bloodstains and the server list.
1. Turn on shadNet's seamless mode: run the shadNet container again with
`-e SHADNET_BLOODBORNE_SEAMLESS_COOP=1` (it is kept in its `shadnet.cfg`).
2. Build and start the game server on the same computer, with the same Tailscale address:
```sh
mkdir bbsc-server && cd bbsc-server
for f in Dockerfile entrypoint.sh; do
curl -fsSLO "https://raw.githubusercontent.com/ayozetr/bloodborne_linux/main/tools/bbsc-server/$f"
done
docker build -t bbport-bbsc-server .
docker run -d --name bbsc-server --restart unless-stopped --network host \
-v bbsc-data:/data -e BBSC_HOST=100.x.y.z bbport-bbsc-server
docker logs bbsc-server
```
`BBSC_SHADNET` is the shadNet server's address when it is on another computer. Messages,
bloodstains and its logs live in the `bbsc-data` volume. Without Docker:
`python3 scripts/bbsc_gameserver/bbsc_gameserver.py --shadnet 100.x.y.z --host 100.x.y.z`.
| Port | Protocol | What |
|---|---|---|
| 20443 | TCP | The game server (messages, bloodstains, ghosts) |
| 20444 | TCP | BBSC's relay (warps, world sync, scaling) |
| 18671 | TCP | Its bbhost-compatible service (`BBSC_BBHOST=0`, `--no-bbhost`, turns it off) |
| 3478 | UDP | The relay BBSC players fall back to when they cannot reach each other directly |
Each player then sets **Game server** to `http://100.x.y.z:20443` in the launcher's
**Multiplayer → Online** tab, keeps the same shadNet server and account, and turns on BBSC in the **Seamless co-op (BBSC)** group.
BBSC finds its relay at the game server's address (or `BBSC_RELAY`, the relay address in BBSC's
settings).
## Troubleshooting
- **The log shows `127.0.0.1` or another address in the listeners:** check `SHADNET_HOST`, or
`Host` in the `shadnet.cfg` beside the server's executable.
- **`Waiting for 100.x.y.z` in the log:** Tailscale is not up on the server, or the address is
wrong (`tailscale ip -4`).
- **The game says it is not signed in to PlayStation Network:** a wrong name or password (code 8 in
the log), or two players using one account.
- **The test passes but summons fail:** every player's computer must be in the tailnet, not only
the server; `tailscale ping <player>` from each side. Everyone needs the game at version 1.09
and the same co-op mode.
See also [Online play and co-op](ONLINE.md) for the bells, passwords and log lines of a summon.
-134
View File
@@ -1,134 +0,0 @@
# Проверка TAA и входов временных апскейлеров — 2026-10-01
## Состояние проекта
bbport — специализированный Linux-рантайм для оригинального x86-64 кода Bloodborne
CUSA03173 1.09 с переводом GPU-команд в Vulkan на основе shadPS4. Это экспериментальный
порт: загрузчик, HLE системных вызовов, звук, ввод, сохранения, игровой интерфейс настроек
и temporal upscaling уже реализованы. В этой рабочей копии доступны исходники, собранная
библиотека, игровой дамп и сохранение. Git-метаданных проекта нет, поэтому историю
изменений и чистоту рабочего дерева проверить нельзя.
Полное прохождение, переносимость на другие драйверы и Steam Deck этой проверкой не
подтверждаются. В документации остаются ограничения: искусственные occlusion query,
неполный predication, прозрачные эффекты без собственных векторов движения и гонки
в модели FSR 4.1.1 для некоторых нестандартных размеров вывода.
## Видео и причины
Исходная запись `video_2026-10-01_21-21-02.mp4`: 94.89 с, 2560×1080, 60 FPS.
В ней переключаются TAA, FSR 4.1.1, FSR 4 и FSR 3.1. На архитектуре, тонких контурах
и движущемся оружии видна временная нестабильность. В меню включён игровой motion blur;
его вклад нельзя приписывать TAA. Файл `video_fixed_taa.mp4` имеет ту же длительность,
но другой размер кадра (2560×1082); сама эта запись не подтверждает исправление рендерера.
Кадры и последовательности для просмотра лежат в `out/dev/temporal-review/`.
Выявлены ошибки в коде, независимо от субъективной оценки видео:
1. **FP16 depth в TAA history.** Шаг half около 1 составляет примерно 0.000488.
При near ≈ 0.05 дальние поверхности становятся неразличимыми. Порог проверки истории
был меньше этого шага, поэтому история могла как ошибочно отбрасываться, так и смешиваться
с другой поверхностью. История теперь RGBA32F.
2. **Несопоставимые камеры.** Старая проверка сравнивала глубину прошлого кадра с глубиной
текущего. Теперь текущая точка переводится в систему предыдущей камеры, и сравнение
выполняется с ожидаемой глубиной там. Допуск масштабируется по перспективной глубине.
3. **Обнуление дальних векторов.** `depth >= 0.99999` считалось небом и получало нулевое
движение. Для проекции с near ≈ 0.05, far 3000 этот порог затрагивает геометрию примерно
от 1875 единиц. Теперь только clear depth 1 считается бесконечным фоном; фон получает
вращательное движение камеры без переноса. Дальняя геометрия получает обычную репроекцию.
4. **FP16 depth в object motion.** В RGBA16F терялась глубина, используемая для проверки,
не перекрыт ли вектор более поздним draw. Буфер и формат графического pipeline теперь
RGBA32F; проверка глубины больше не принимает другую дальнюю поверхность с широким
фиксированным допуском. Финальные векторы для FSR по-прежнему RG16F.
5. **Проверка смешанной глубины на краях.** TAA интерполировал глубину истории между
разными поверхностями. Теперь четыре bilinear tap проверяются независимо, цвет собирается
только из согласованных tap; при частичном покрытии вес истории уменьшается.
6. **Слишком слабое накопление вдали.** Прежнее depth-adaptive снижение веса до 0.8/0.5
заменено весом 0.98…0.85 в зависимости от движения при валидной истории (нижний
вес достигается при 8 px/frame). Диагностический прогон с весом 0.94 показал
остаточные колебания цвета между фазами jitter даже с принятой историей. RGB истории
ограничивается диапазоном соседей текущего кадра.
7. **Jitter глубины на склонах и тонких деталях.** Глубина теперь переносится в
стабильную позицию пикселя по локальному одностороннему градиенту, с защитой от
разрывов поверхности. Для motion/depth выбирается ближайшая поверхность в 3×3;
цвет и диапазон clipping берутся вокруг исходного цветового пикселя. Это позволяет
тонким контурам сохранять историю при чередовании foreground/background под jitter.
8. **Нестабильная адресация истории при почти нулевом движении — основная причина
обнаруженного дребезга TAA.** Раньше целая и дробная части bilinear-координаты
вычислялись после сложения большого номера пикселя с очень маленьким motion.
На GPU это давало несогласованные части и выбор соседнего texel. Теперь сначала
разделяется сам motion: `base = p + ivec2(floor(motion))`,
`fraction = motion - floor(motion)`. Регрессионная проверка на пикселе x=2010
с движением около −0.00003 px воспроизводит ошибку старой формулы:
цвет 0.250000 вместо ожидаемого 0.739985. Исправленная формула проходит проверку.
Исправления 3–4 относятся к общим входам TAA/FSR 3.1/FSR 4/FSR 4.1.1. Алгоритмы и
ML-ассеты AMD не изменялись. Проверка ABI shader/pipeline и диагностический dump object
motion обновлены вместе с форматом ресурса.
## Проверки и ограничения
Сборка `bbgpu`, `taa-shader-test`, `camera-motion-test` через `shell.nix`.
На Vulkan GPU прошли 18 случаев production TAA shader: reset с NaN, unjitter, накопление,
clipping, раскрытие поверхности, выход за кадр, NaN history, точная дальняя глубина,
отказ другой дальней поверхности, перенос камеры, край с частично валидной историей,
небо и граница небо/геометрия, наклонная глубина при обоих знаках jitter, тонкий контур,
снижение persistence при движении и очень малое отрицательное движение на большой координате.
Прошли 6 проверок production camera-motion shader:
дальняя геометрия, вращение неба, отсутствие переноса неба, исключение jitter,
валидный и устаревший object vector. Лог: `out/dev/temporal-review/tests.log`.
Дополнительная проверка unjitter: `out/dev/temporal-review/taa-final-tests.log`.
Воспроизведение регрессии старой формулы и 18 успешных проверок новой:
`out/dev/temporal-review/regression-proof.log`.
Финальная проверка игры: `out/dev/temporal-review/subpixel-validation.log`.
Все 64 существующих Python-теста прошли (`out/dev/temporal-review/python-tests.log`).
Live smoke: копия сохранения загружена в Сне Охотника. В одном процессе проверены
TAA → Off → FSR 3.1 → FSR 4 v07 → FSR 4.1.1 → TAA, вывод 2560×1440,
по шесть финальных кадров на режим и движение камеры между переключениями.
FSR работают с входом 1706×960; TAA — с нативным 2560×1440. Контексты AMD
подтверждены логом, подмены на FSR 3.1 не было. Процесс завершился штатно;
после компиляции pipeline выдерживал лимит 60 FPS. Логи и raw-кадры:
`out/dev/temporal-review/subpixel-validation.log`, `out/dev/temporal-review/far-refined/`.
Игровые motion blur и AA отключены в тестовой конфигурации.
При неподвижной камере на участке дальней башни (x=2010…2099, y=400…779)
среднее абсолютное изменение RGB между шестью кадрами TAA снизилось с 1.892 до
0.299 по шкале 0…255, примерно в 6.3 раза. После возврата к TAA — 0.279.
В финальном прогоне FSR 3.1: 0.328, FSR 4: 0.278, FSR 4.1.1: 0.364.
Это результат выбранной сцены, а не универсальная оценка качества: разрешение входа
FSR отличается от TAA, а до/после здесь сравнивается именно последняя правка
адресации истории. Данные: `out/dev/temporal-review/far-refined/stability.json`;
предыдущий прогон: `out/dev/temporal-review/final-validation.log`.
Для диагностики добавлен `BB_TAA_DIAGNOSTICS=1`: альфа выхода TAA содержит вес принятой
истории вместо 1. Его можно прочитать в `BB_DUMP_TRIGGER` raw RGBA16F. RGB не меняется,
альфа истории по-прежнему содержит depth. Обычный режим сохраняет непрозрачный выход.
Дополнительные режимы 2 и 3 выводят текущий цвет или ограниченную историю;
диагностический образ истории позволяет сравнить прямое чтение с репроекцией.
Эти сравнения выявили ошибку адресации даже при принятой истории и почти нулевом motion.
Исходники и `out/gpu/libbbgpu.so` обновлены. Пересобран
`dist/Bloodborne-bbport-x86_64.AppImage` (832 MiB); пакетный `--vulkan-info`
успешно определил RX 7800 XT через RADV. Логи: `appimage-build.log` и
`appimage-vulkan.log` в `out/dev/temporal-review/`. Полный игровой прогон выше
выполнен с локальной библиотекой; отдельный полный игровой прогон AppImage не выполнялся.
Эти тесты подтверждают конкретные исправления, но не доказывают исчезновение любого
мерцания в игре. Движение объектов по истории вершин, прозрачности, анимированные
материалы, выбор LOD и субпиксельная геометрия при сильном уменьшении разрешения
по-прежнему требуют проверки в отдельных сценах.
Цена полной точности: две истории TAA занимают 253 MiB вместо 127 MiB в 4K;
object motion — 127 MiB вместо 63 MiB при 4K render. Это также повышает расход
пропускной способности памяти. Возможная следующая оптимизация — отдельный R32F depth
при сохранении цвета и motion в FP16, с теми же тестами корректности.
Для регрессий:
```sh
nix-shell shell.nix --run 'ninja -C out/gpu taa-shader-test camera-motion-test && out/gpu/taa-shader-test && out/gpu/camera-motion-test'
```
Подход с движением для всех входных пикселей согласуется с рекомендациями
[AMD FSR 3.1 integration](https://gpuopen.com/presentations/2024/FidelityFX_Super_Resolution_3-1_Release-Overview_and_Integration.pdf).
+99
View File
@@ -0,0 +1,99 @@
# Our own translator instead of shadPS4: structure and order of work (2026-10-10)
> Design notes from when this work was planned, kept for their reasoning. bbport 0.7 has one
> memory model and its own renderer, and some switches named here (`BB_CP_SHADOW` and `BB_CP_DECODER`) no longer exist; the
> current list is in [ENVIRONMENT.md](ENVIRONMENT.md).
Goal: move away from emulation and from shadPS4 code. We want our own PS4 command translator and
our own graphics renderer that scale to other games (everything specific to one game lives in its
profile, `gpu/games/`) and behave the same on AMD and NVIDIA. After that comes shadnet (online play).
## Current state
Borrowed from shadPS4 (`gpu/VENDOR.txt`, commit b4e7ae7 from 2026-09-24): about 116k lines.
| Part | Lines | What it does | Our work in it |
|---|---|---|---|
| `video_core/amdgpu` | 8.3k | PM4 parsing (command processor `liverpool.cpp`), registers, tiling | GPU-side labels, occlusion, predication, PM4 self-test |
| `video_core/renderer_vulkan` | 28.6k | Vulkan renderer: pipelines, rasterizer, scheduler, presenter | draw pipeline (stages A/B), recording threads, upscalers, background compilation |
| `video_core/buffer_cache` | 7.4k | game memory for the GPU | almost all rewritten (memory module, VRAM copies, traps) |
| `video_core/texture_cache` | 4.8k | textures by address, detiling | time-based collector, uploads |
| `shader_recompiler` | 38.6k | GCN → IR → SPIR-V | paged buffers (BDA), fixes |
| `core`, `common` | 21.8k | VideoOut, GnmDriver, event queues, utilities | vblank, labels |
Our own code, outside shadPS4: `gpu/layer/` (memory module `BbLayer::GpuMemory`), `gpu/games/` (game
profiles), `gpu/shim/` (overlay, settings, hooks), the runtime in `src/` (kernel, libc, files, saves).
## Target structure
Modules with narrow interfaces. None of them knows addresses in the game's code (that belongs to the
profile), and none does the GPU's work on the CPU (that is emulation):
1. **memory**: game memory in place, VRAM copies as a cache, shared tracking of page protection
(write traps for copies and textures). Exists: `layer/` + the module in `buffer_cache_layer.cpp`.
2. **cp**: command processor. Parses PM4 into the translator's command stream (state, draws,
dispatches, synchronization) in the order the GPU executes it. Replaces `liverpool.cpp`.
3. **state**: context registers → pipeline keys and Vulkan dynamic state. Replaces
`liverpool_to_vk` and the key building in `vk_pipeline_cache`.
4. **resources**: V#/T#/S# descriptors → buffers, images, samplers (caches, detiling). Replaces
`buffer_cache`, `texture_cache` (the buffer part is already almost ours).
5. **render**: Vulkan. Command recording per thread, barriers, submission, presentation. Replaces
`renderer_vulkan` (the scheduler and stages A/B are already ours).
6. **shaders**: GCN → SPIR-V. The largest part; it is replaced last, stage by stage (decoder,
IR, passes, code generation).
7. **platform**: VideoOut, GnmDriver, kernel events (`core/libraries`), next to the runtime.
8. **games**: game profiles (exists).
## Order
One module is replaced at a time. First an interface in our code with the shadPS4 code behind it
as is, then our own implementation behind the same interface. After each step: the route (a route
benchmark script (not in this repository), frames and snapshots), the PM4 self-test
(`BB_PM4_SELFTEST=1`), unit tests, the game on AMD and (from tester logs) on NVIDIA.
| Step | What | Why in this order |
|---|---|---|
| 1 | memory: shared tracking of page protection; write traps under textures by default; model 0.3 and the sparse arena are removed | removes the rest of memory emulation and the dependency on game hooks |
| 2 | cp: the translator's command stream; `MEM_SEMAPHORE`, GPU time, compute queues done by the GPU | the last emulation left is in the command processor |
| 3 | state + resources | they build on 1 and 2; the buffer part is already ours |
| 4 | render | the scheduler and stages are already ours; the rasterizer and pipelines remain |
| 5 | shaders | the largest and riskiest; by then everything else is already ours |
| 6 | shadnet | networking, once the graphics are our own |
Steps 1 and 2 are the plan to move away from emulation
([`EMULATION_REMOVAL_PLAN.ru.md`](https://github.com/deadinside28/bloodborne_pc/blob/master/docs/EMULATION_REMOVAL_PLAN.ru.md),
section dated 2026-10-10).
## Progress
### 2026-10-10: our own PM4 decoder (step 2, start)
- `gpu/cp/`: our own code, without shadPS4. Packet format and opcodes (`cp_pm4.h`), register file and
decoder (`cp_decoder.{h,cpp}`). The decoder walks the buffer in stream order, applies register
packets (`SET_*_REG`, `CLEAR_STATE`, registers written by draw packets), descends into
nested buffers and hands each packet to a "sink" (`BbCp::Sink`) with the state already applied:
draws and dispatches as typed calls, the rest as packets. The sink decides `COND_EXEC`. The
decoder knows nothing about Vulkan, the game or where execution happens.
- Unit test `tests/test_cp_decoder.cpp` (target `cp-decoder-test`): registers, partial writes,
draws, dispatches, nested buffers, `COND_EXEC`, `CLEAR_STATE`, corrupt buffers.
- Register addresses are checked against the shadPS4 layout at build time (`static_assert`). This
found a bug: on CIK, `CP_STRMOUT_CNTL` is in uconfig (`0xC03F`), not in config.
- Shadow check in the game (`BB_CP_SHADOW=1`): the new decoder parses every top-level buffer
from the same state, and the register files are compared at the end of the buffer. On the route,
all buffers matched (~800k buffers, ~200 million packets, 0 mismatches, 0 parse errors).
Bloodborne does not use nested buffers (they are covered by the unit test).
- Next: the decoder as a cursor (packet by packet, so the command processor loop can yield to
other queues while waiting), the register file as shared state storage (its layout matches
shadPS4 `Regs` word for word), then moving packet handling from `liverpool.cpp` into the
translator's sink one packet type at a time, each time with the shadow check and the route.
### 2026-10-10: the new decoder is the real parsing path
- The decoder became a cursor (`BbCp::Cursor`: packet by packet, nested buffers at the
caller's choice). The register file can live in the owner's storage (for the command processor,
its `regs`, with the same word layout). A write observer (`RegisterObserver`, one call per
packet) provides what draw preparation needs: the written blocks and the packet checksum.
- `ProcessGraphics` takes packets and register writes from the cursor (`BB_CP_DECODER=0` selects
the old walk). What each packet does is still the old code. PM4 self-test: 11 of 11. The share of
prepared draws is the same (747k used, 19k not, versus 755k/24k), so the checksums
match. The image is the same.
+121
View File
@@ -0,0 +1,121 @@
# Troubleshooting
When asking for help, attach the log: in the launcher's **Log** section, **Export log…** saves it to
a file and **Copy** puts it on the clipboard.
## The launcher rejects the game
The launcher checks the game folder before starting. What it says, and what to do:
| Message | What to do |
|---|---|
| bbport needs the 1.09 update merged into the game folder | Copy everything from the dumped 1.09 update into the game folder, replacing files (`eboot.bin` and `sce_sys` too). |
| param.sfo is from 1.09 but eboot.bin is not | Copy `eboot.bin` from the dumped 1.09 update over the old one. |
| This eboot.bin has the 60 fps patch by Lance McDonald applied | Use a clean `eboot.bin` from the 1.09 update. bbport has its own 60 FPS. |
| This is not a retail release of Bloodborne | Alpha tests and other games do not run. See [supported releases](COMPATIBILITY.md). |
| eboot.bin could not be read as a decrypted PS4 executable | Dump the game and the update again. |
| The game files are damaged | The extraction tool broke compressed files (LibOrbisPkg PkgTool on .NET 6 or newer); the game would hang while loading. Extract the game and the update again with a fixed tool. |
## The game shows *New Game* instead of my save
Saves live in the data directory, and each way of running the port has its own:
- The AppImage: `~/.local/share/bbport/user/savedata/1/<title ID>/`.
- From the repository (`run.sh`): `user/savedata/1/<title ID>/` in the repository.
To move a save, close the game, back up both folders, and copy the `savedata` folder over.
## DLSS is "not supported by this GPU"
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 (`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`.
## Spanish shows the Latin American text (or the other way round)
The game picks a language's regional text by the dump's region and the system language. Choose the
text you want in **Setup → Game text language**: *Español* for Spanish from Spain, *Español
(Latinoamérica)* for Latin American Spanish. **Voice language** does the same for the voices.
## The game crashes after playing a while, on a card with little VRAM
On a 6 GB card the texture cache can run out of video memory: the log then repeats
`Texture cache: memory pressure, ... (critical ...)`. Lower what uses VRAM: a 60 FPS limit,
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 picture is stretched on an ultrawide screen
Choose the screen's shape in **Graphics → Display → Aspect ratio** (*Auto* follows the output
resolution): 4:3, 16:10, 16:9, 21:9 or 32:9. The view gets wider (or taller) instead of stretched,
and the HUD keeps to the screen's edges; it changes live, in the game's *Display* menu too.
**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. 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
From bbport 0.7, L3 + R3 opens the port's menu only when both are held for 0.8 seconds, so pressing
them in a fight (lock-on and camera reset) no longer opens it. **Controls → Port menu from the
controller** can make it 2 seconds, or turn it off so that only Insert on the keyboard opens the
menu. Closing it with L3 + R3 always works.
## The controller does nothing when the game is started from Steam
With Steam Input on, Steam hides the controller and the game gets Steam's virtual one, whose input
Steam sends only to the window it sees focused. On a Wayland desktop Steam sees only X11 windows,
so the game now opens its window through Xwayland when Steam made a virtual controller for it (the
log says `Window: X11 (Xwayland), so Steam Input reaches the game`). On older releases, put
`SDL_VIDEO_DRIVER=x11 %command% --play` in the shortcut's launch options (keep `--play` after
`%command%`), or turn Steam Input off for the shortcut: the game reads the controller itself.
## The Steam overlay does not open in the game
Steam's overlay (Shift+Tab, its FPS counter, F12 screenshots) needs the game started from Steam,
with **Steam overlay** on under **Add to Steam** in the launcher (on by default). The log then says
`Steam overlay: …/ubuntu12_64/gameoverlayrenderer.so`. Without that line, the game was not started
by Steam.
## The camera turns much faster on diagonals
With the right stick pushed all the way, the game switches to its fast turn (about twice the
speed), and it does so on diagonals too, not only straight sideways. That is how the game turns on
a PS4 as well, with any controller. **Controls → Even camera speed on diagonals** keeps diagonals at
the normal speed; the fast turn stays when the stick is pushed straight sideways. With it on the
camera can feel heavier: the game's own *Camera speed* setting makes up for it.
## Summons never connect
Press **Test** in the **Status** row of **Multiplayer → Online**. An error mark on the shadNet server's UDP line means
the server never learns your address, and no summon can connect: check that the server is running
and that its log lists the UDP listener, and that no firewall blocks UDP 31314 on its side
([hosting your own server](SERVER.md)). shadPS4's public server does not accept bbport builds at all: use The
Hunter's Dream, Seamless Hunt or your own server.
## Other online problems
See [Online play and co-op](ONLINE.md): the account, the bells, the ports and the log lines of a summon.
+68
View File
@@ -0,0 +1,68 @@
# FAQ
## Is this an emulator?
No. The game's own code runs directly on the CPU, and its graphics are translated to Vulkan. It is
built on [bbport](https://github.com/deadinside28/bloodborne_pc) by deadinside28.
## Where do I get the game?
From your own PS4: a dump of your copy with update 1.09. No game files are included, and none can
be shared here.
## Can I play with Windows players?
Yes. On Seamless Hunt or The Hunter's Dream, Linux and Windows players play together; for seamless
co-op everyone needs the same BBSC version (0.0.2). [Online play](ONLINE.md).
## Summons take a long time
Some waiting is normal: the game checks for summon signs about every 35 seconds, as the PS4's
servers had it, so a sign can take over half a minute to show up.
## My friend never gets summoned with the Kin Bell
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** 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?
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?
Turn on *Advanced → Developer → Save the log and statistics to a file* once in Desktop Mode; Game
Mode sessions then save theirs too. [Getting a log](logs.md).
## The game updated and my Steam shortcut stopped working
Press **Add to Steam** again in the launcher: it updates the shortcut.
+41
View File
@@ -0,0 +1,41 @@
# Getting started
## What you need
- **Your own dump of Bloodborne with update 1.09**, from your PS4: any retail release (US, Europe,
UK, Japan, Asia, and the editions with The Old Hunters). No game files are included, and other
versions of the game's executable start and then crash. [Which releases work](COMPATIBILITY.md).
- **Linux x86-64** and a GPU with **Vulkan 1.3**: AMD or Intel with Mesa, or NVIDIA with NVIDIA's
driver. The Steam Deck works too.
- About **6 GB of free memory**, 10 GB for 1440p or 4K.
## First launch
1. Download `Bloodborne-bbport-x86_64.AppImage` from the
[latest release](https://github.com/ayozetr/bloodborne_linux/releases/latest).
2. Make it executable (in a file manager: *Properties → Permissions → Allow executing*, or
`chmod +x Bloodborne-bbport-x86_64.AppImage`) and run it.
3. In the launcher, choose your **game folder**: the one with `eboot.bin` in it.
4. Press **Play**.
!!! tip "The first minutes"
The first time, the game builds its shaders as it meets them, so it can stutter for a few
minutes. The next sessions start with them ready.
## Saves, settings and the DLC
- Saves and settings go to `~/.local/share/bbport`.
- **Setup → Saves** exports your saves into a `.zip` and imports them back, also from shadPS4.
- The Game of the Year edition already has *The Old Hunters*. With the base game, put its DLC
folder in **Setup → DLC folder**.
## Updating
The launcher's **About** page says when a new release is out and installs it; it can also go back
to an earlier one. Your saves and settings stay as they are.
## Next steps
- [Play online](ONLINE.md), alone or in seamless co-op.
- [Add the game to Steam](steam-deck.md), or play on the Steam Deck.
- Something wrong? [Troubleshooting](TROUBLESHOOTING.md) and [getting a log](logs.md).
-67
View File
@@ -1,67 +0,0 @@
# Исправления — 2026-09-27
## ✅ Что исправлено
### 1. Маска реактивности не сохранялась
- **Файл:** `gpu/shim/bbport_settings.cpp`
- **Проблема:** Чекбокс "Показать маску (отладка)" не сохранялся в `bbport.ini`
- **Решение:** Добавлена загрузка/сохранение параметра `debug_view`
### 2. 🔥 ДИНАМИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ПРЕСЕТОВ на RX 7800 XT
- **Файлы:**
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp`
- `gpu/shim/bbport_overlay.cpp`
- **Проблема:** RX 7800 XT не поддерживает `blit D32S8`, поэтому требовался перезапуск
- **Решение:**
- Fallback: UI depth очищается вместо копирования (работает корректно)
- Убрана блокировка `BB_LIVE_SCALING_UNSUPPORTED`
- Обновлено меню — убрано требование перезапуска для пресетов
### 3. Правильная конфигурация
- **Файл:** `bbport.ini`
- **Критично:** `jitter=1` (БЫЛО ВЫКЛЮЧЕНО!)
- Оптимизированы параметры резкости и маски
---
## 🎮 Что теперь работает БЕЗ перезапуска
- ✅ **Смена пресетов** (Native AA / Quality / Balanced / Performance / Ultra Performance)
- ✅ Включение/выключение апскейлера
- ✅ Резкость
- ✅ Jitter
- ✅ Маска реактивности
- ✅ Отладочные режимы
**Перезапуск нужен ТОЛЬКО для:** векторов движения объектов (`object_motion`)
---
## 🧪 Тестирование
### Сборка
✅ Успешно
### Проверить в игре:
1. **Динамическое переключение:**
- Открыть меню (Insert или L3+R3)
- Сменить пресет
- **Должно применится СРАЗУ**
2. **Шлейфы на оружии:**
- С `jitter=1` должны стать НАМНОГО меньше
- Сравнить с `object_motion=0`
3. **Маска реактивности:**
- Должна отображаться (красный цвет)
- Помечены прозрачные эффекты
---
## 📝 Изменённые файлы
- `gpu/shim/bbport_settings.cpp`
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp`
- `gpu/shim/bbport_overlay.cpp`
- `bbport.ini`
-248
View File
@@ -1,248 +0,0 @@
# Bloodborne на ПК: фактическое состояние порта и апскейлера
Срез на **29 сентября 2026 года**. Источник — файлы в `game_files`, история Git
`native_probe` до `ac9fe90`, текущие незакоммиченные изменения, сохранённые логи
запусков и локальные тесты. В документе «есть в коде», «запускалось» и «проверено
по изображению» не считаются одним и тем же.
## Краткий итог
Это работающий экспериментальный **нативный Linux-порт**, а не запуск через
полный эмулятор: исходный x86-64 `CUSA03173/eboot.bin` исполняется на CPU ПК,
системные вызовы PS4 обрабатывает собственный HLE-рантайм, графику выводит
Vulkan-ядро на основе shadPS4. Игра доходит до сцены с персонажем, принимает
ввод, работает со звуком и сохранениями; это подтверждают логи запуска,
снимки и видео в корне `native_probe`. Прохождение всей игры, все локации и
отсутствие сбоев этими материалами **не подтверждены**.
**FSR 3.1 temporal upscaling реально работает по проверенному пути с разрешением
сцены, заданным при старте**: лог `логи.md` показывает `960×540 → 1920×1080`,
компоновку UI в 1920×1080 и запуск на RX 7800 XT. Текущий путь смены пресета
без перезапуска остаётся проблемным на этой видеокарте: последний
`last_run.log` показывает переключение запрошенных размеров сцены, но FSR-контекст
`1920×1080 → 1920×1080`. По коду уменьшенные цели здесь не выбираются, когда
`D32S8` не поддерживает blit. Следовательно, лог переключения пресетов **не
доказывает реальное снижение разрешения 3D и выигрыш FPS**.
## Состав проекта и развитие
| Путь | Роль |
|---|---|
| `../CUSA03173/` | Локальные файлы игры, `eboot.bin`, ресурсы и модули PS4. |
| `../np_base/` | Снимок ранней базы порта; рабочая разработка идёт в `native_probe`. |
| `prepare.py`, `link_libc.py`, `link_modules.py`, `probe.c`, `runtime_*.c` | Подготовка исполняемого образа, привязка модулей, загрузчик и HLE. |
| `gpu/` | Vulkan-видеоядро, рекомпилятор шейдеров, доработки для игры, FSR и ImGui. Заимствованное ядро shadPS4 описано в `gpu/VENDOR.txt`. |
| `patches/Bloodborne.xml`, `patches.py` | Патчи FPS и разрешения, применяемые к гостевому коду при старте. |
| `bbport.ini`, `run.sh`, `build.sh` | Настройки, полный запуск и сборка. |
| `out/`, `out/dev/`, `last_run.log`, `логи.md` | Собранные бинарники и следы проверок; часть логов относится к более ранним версиям кода. |
Развитие по Git: 25 сентября добавлены загрузчик, HLE, графика, звук и
оптимизации CPU/GPU; 26 сентября — анализ кадра, векторы камеры, jitter,
FSR 3.1, пресеты и меню; 27 сентября последний коммит `ac9fe90` исправил
переключение пресетов и отображение маски. После него в рабочем дереве есть
незакоммиченные доработки векторов объектов, уменьшенных целей рендера,
разделения разрешений сцены/UI, скриптов и тестов. Это **текущее рабочее
дерево**, не готовый релиз. Старые разделы `README.md`, `docs/upscaler.md`
и `docs/ROADMAP.md` описывают разные этапы и местами уже устарели.
## Что реализовано
### Запуск и производительность
- Гостевой `eboot` и часть оригинальных PRX запускаются вместе с реализациями
памяти, потоков, файлов, времени, контроллера, аудио, сохранений и сервисов.
Сеть работает в офлайн-режиме; PSN не реализован.
- SDL3 обслуживает геймпад и запасное управление с клавиатуры. Звук выводится
через host-аудиосистему; в коде есть исправление темпа `AudioOut` и флаг
сохранения для отсутствовавших звуков.
- Vulkan-рендерер получил отдельный поток записи команд, подготовку draw на
четырёх потоках, кеши текстур и буферов, копирование гостевой памяти и пул
staging-буферов. `docs/parallel_gpu.md` и `docs/ROADMAP.md` содержат
замеры в отдельных сценах; их нельзя переносить на все конфигурации.
- `BB_FPS=uncap` по умолчанию применяет патч с шагом симуляции по времени
кадра; доступны `30`, `60`, `90`. Частота вывода ограничивается монитором
и максимумом 120 FPS. Для диагностики есть `BB_FRAME_STATS=1`,
`BB_TOGGLE_FILE` и захват структуры кадра через `BB_CAPTURE_TRIGGER`.
### Апскейлер
- В игре нет готового буфера скорости. Порт вычисляет векторы камеры из
глубины и матриц двух кадров; для геометрии сцены делает субпиксельный
jitter Halton. Перепроекция статичной геометрии проверялась ранее
(`docs/upscaler.md`).
- Интегрирован FSR 3.1 через `gpu/third_party/fsr-vulkan`. Native AA работает
1:1 на HDR-цвете сцены перед постобработкой. Проверенный режим с пониженным
разрешением использует патч гостевого разрешения, FSR после тонмаппинга и
отдельную компоновку интерфейса в 1920×1080. Патч UI в текущем дереве
оставляет его координаты нативными. Явный `BB_RENDER_RES=ШxВ` включает
этот путь; например, лог `логи.md` подтверждает `960x540`.
- Пресеты в коде: Native AA `1920×1080`, Quality `1280×720`, Balanced
`1130×636`, Performance `960×540`, Ultra Performance `640×360` для вывода
1920×1080. Меню ImGui открывается `Insert` или `L3+R3`. В `bbport.ini`
сохраняются FSR, пресет, резкость, jitter, маска реактивности, режим
отладки, векторы объектов и счётчик FPS. Текущий конфиг содержит
`upscaler=fsr3`, `preset=4`, `jitter=1`, `reactive=1`,
`object_motion=1`, `debug_view=0`.
- Маска реактивности формируется сравнением цвета до и после прозрачных
проходов. Она опциональна: прежние наблюдения отмечают дрожание тонкой
дымки при её включении. Настройка по умолчанию в коде выключена, но
в текущем `bbport.ini` включена.
- В рабочем дереве есть экспериментальные векторы анимированных объектов:
прошлые позиции вершин, дополнительная цель G-buffer, сопоставление
отрисовок между кадрами и ограничение на подходящие буферы позы/костей.
Изменение `object_motion` требует перезапуска. По
`out/dev/motion-selective.log` история в выбранных draws часто находится,
без пропусков по ёмкости в показанных интервалах. Это метрика draw, **не
оценка покрытия пикселей или визуального качества**.
## Текущий дефект динамических пресетов
В `vk_scene_resolution.cpp` функция `SceneTargets::Eligible` требует
`BlitSrc|BlitDst` у формата каждого кандидата. В `vk_rasterizer.cpp`
уменьшенный проход отключается, если неподходящая глубина; от неё же зависит
выбор уменьшенного входа FSR в `vk_temporal_upscaler.cpp`. Сохранённый лог
для RX 7800 XT сообщает об отсутствии `blit` у `D32S8`. Позднее добавлен
fallback, который **очищает глубину UI** вместо её копирования, но он не
устраняет требование `blit` у глубины **3D-сцены**.
Итог на этой машине: в `last_run.log` 28 сентября перечислены переключения
`640×360`, `1280×720`, `960×540` и других пресетов, однако созданный
FSR-контекст остался `1920×1080 → 1920×1080 (HDR scene color)`. Сообщение
`Scene resolution: raster ...` отражает запрошенный размер proxy-целей;
оно не измеряет фактически использованный размер каждого draw. Заявления
`README_FIXES.md` и подсказки меню о работающем на всех GPU динамическом
масштабировании сильнее, чем позволяют код и последний лог. `логи.md`
относится к более раннему запуску с автоматическим выбором стартового патча,
который затем изменили в `run.sh`.
## Проверки и границы уверенности
| Проверка | Результат |
|---|---|
| Логи игрового запуска и кадры от 27–28 сентября | Загрузка в игровую сцену и вывод изображения подтверждены. Сквозной тест прохождения не проводился. |
| `логи.md` на RX 7800 XT | Подтверждены патч сцены `960×540`, UI `1920×1080`, FSR-контекст `960×540 → 1920×1080`. |
| `last_run.log` | Меню меняло пресеты без перезапуска, но FSR был 1:1; FPS-статистики этого запуска нет. |
| `out/dev/motion-selective.log` | Для одной серии сцен с выборочными векторами объектов видны интервалы примерно 72–100 FPS, без `capacity skips` в приведённых сводках; это не контролируемое сравнение пресетов. |
| `out/gpu/motion-history-test` и `out/gpu/ui-composition-test` | Повторно выполнены 29 сентября: оба `PASS`. |
| `nix-shell shell.nix --run 'python3 -m unittest -v test_patches'` | Повторно выполнено 29 сентября: 5 тестов `OK`. |
| `out/dev/scene-validation.log` | Vulkan-тест уменьшенных целей на Lavapipe **завершился assertion** из-за несовпадения версии заголовков Vulkan и диспетчера. Полной успешной проверки этого пути лог не даёт. |
Векторы объектов в игре и их эффект на шлейфы оружия и одежды визуально не
подтверждены систематическим A/B-тестом. Ранние замеры около 49 FPS со
всеми draws против 77–78 FPS без них и около 81–87 FPS на более узкой
выборке относятся к разным запускам; это ориентир для дальнейшего теста,
а не гарантированная производительность текущего конфига. Снимок
`image_2026-09-28_00-44-14.png` показывает игровую сцену и оверлей около
59 FPS, но не раскрывает условия замера.
## Что ещё не сделано / что проверять следующим
1. Исправить и подтвердить реальный уменьшенный 3D-рендер в динамическом
режиме на RX 7800 XT: проверить фактический размер color/depth attachments
и FSR-вход на каждом пресете. Если путь остаётся несовместимым с `D32S8`,
явно выбирать стартовый патч разрешения и показывать в меню необходимость
перезапуска.
2. После исправления провести одинаковую игровую сцену с Native AA,
Quality и Performance: зафиксировать вход/выход FSR, FPS, время кадра,
чёткость UI и изображение в движении.
3. Визуально сравнить векторы объектов включено/выключено при беге, повороте
камеры и в бою; проверить шлейфы, покрытие истории и стоимость GPU.
4. Разобраться с assertion в `scene-resolution-test`; остальные тесты не
заменяют игровой Vulkan-тест. Генерации кадров, FSR 4, DLSS и XeSS в
текущем порте нет — они перечислены в `docs/ROADMAP.md` как планы.
## Как воспроизвести подтверждённый путь
Из `game_files` на этой конфигурации:
```bash
BB_RENDER_RES=960x540 BB_FRAME_STATS=1 bash native_probe/run.sh
```
`BB_RENDER_RES` выбирает патч стартового разрешения сцены, а текущий
`patches.py` сохраняет UI в 1920×1080. Смена разрешения таким способом
требует нового запуска. Без `BB_RENDER_RES` включается экспериментальный
динамический путь, описанный выше. Запуск пересобирает порт и использует
локальные файлы `CUSA03173`; наличие исходников и бинарника само по себе
не подтверждает воспроизводимость на другом ПК.
## Исправление 29 сентября: живая смена пресета
Причин, по которым смена пресета в меню не меняла картинку, было две:
1. `SceneTargets` копировал цели между нативным и уменьшенным размером только
через `vkCmdBlitImage`, а RADV не даёт blit для `D32S8`. Поэтому глубина
сцены считалась неподходящей и уменьшенный рендер не включался. Теперь для
depth/stencil-форматов без blit копирование идёт полноэкранной отрисовкой
(`host_shaders/depth_resample.frag`): глубина пишется через `gl_FragDepth`,
трафарет через `VK_EXT_shader_stencil_export`.
2. Маска реактивности (`reactive=1`) снимала цвет в 1920×1080 и пересоздавала
FSR-контекст 1920→1920, а основной проход сразу пересоздавал его под
уменьшенный вход. Контекст пересоздавался каждый кадр. Теперь маска
читает уменьшенную цель сцены.
Проверка на RX 7800 XT (`out/dyn-res-test.log`): переключения в меню дают
`FSR 3 context 1280x720 / 1130x636 / 960x540 / 640x360 -> 1920x1080`,
по одному пересозданию на смену пресета, без сбоев. FPS в этой сцене около
54–56 на всех пресетах, то есть узкое место здесь не в разрешении 3D.
## 30 сентября: FSR 4 INT8 и производительность
### FSR 4 (v07 INT8/DOT4)
- `vk_fsr4.cpp/.h`: провайдер FSR-Vulkan `ffxFsr4V07*` с моделью под пресет (native,
quality, balanced, performance, ultraperf), тир 1080. Входы те же, что у FSR 3.1.
В меню «Апскейлер → FSR 4 (INT8)», в ini `upscaler=fsr4`, env `BB_UPSCALER=fsr4`.
Если нет ассетов или возможностей GPU, автоматически включается FSR 3.1.
- Ассеты: `fetch_fsr4_assets.sh` скачивает набор, собранный Q2RTX из MIT-исходников
FidelityFX SDK (`fsr4_shaders/`, в `.gitignore`, 98 SHA-256 сверены с манифестами).
`BB_FSR4_DIR` задаёт другой каталог.
- Устройство: включены `shaderIntegerDotProduct` и `VK_KHR_compute_shader_derivatives`
(`Instance::IsFsr4Int8Supported`).
- Проверено на RX 7800 XT: модели quality/ultraperf создаются, при смене пресета
фриз ~6,5 с (компиляция). Пользователь: картинка чётче FSR 3.1, но сильный гостинг
на персонаже и оружии. Векторы проверены отладочным видом, они корректны:
персонаж получает векторы объекта. В SPIR-V pre-прохода глубина выбирается
по минимуму (как у нас), векторы трактуются как «прошлая − текущая». Для A/B
в меню: «FSR 4: авто-экспозиция», «FSR 4: обратный знак jitter».
### Производительность (perf, поток GPU-команд)
- `SceneTargets::Eligible` спрашивал у драйвера свойства формата на каждую отрисовку:
~13–15% времени потока GPU. Теперь кеш по формату.
- `ObjectMotion::Attach`, снимок цвета и маска реактивности вызывали
`Scheduler::CommandBuffer()`. Это переводило запись всего остатка сабмита (почти
весь G-buffer) на поток GPU. Теперь они идут через `Scheduler::Record`.
Остались два прямых вызова в конце кадра (`TemporalUpscaler::Run`, презентер).
- Итог: поток GPU ~74% вместо ~94% одного ядра. В игровых сценах 61–98 FPS против
53–87 до исправлений (сцены не совпадают один в один, это ориентир).
### Профиль с DWARF-стеком (perf, поток GPU-команд, 30.09)
- `Motion::IndexedRange` (скан и хеш индексов для векторов объектов) занимал ~10% потока
GPU. Добавлен `Motion::IndexRangeCache`: результат переиспользуется и полностью
перепроверяется раз в 32 кадра. В игре ~98% диапазонов из кеша (260 записей).
- `SceneTargets`: плоская таблица свойств форматов и память последних восьми целей;
вместе с Attachment/Get/View/Eligible ~4% вместо ~7%.
- После этого поток GPU загружен работой без заметных ожиданий. Поток записи Vulkan
~40% работы и ~57% ожидания. GPU (RX 7800 XT) в среднем ~65% занятости.
Ограничивает кадр поток GPU-команд. Главные статьи — базовые затраты видеоядра:
FindView ~5,6%, BindTextures ~5,6%, memmove ~4,6%, SmallGuestCopy ~3,5%,
CachedImageDescEntry ~3,2%, BindBuffers/BindVertexBuffers ~3% каждая.
Следующий шаг — перенос этих вычислений на рабочие потоки (шаг 2 parallel_gpu.md).
### FSR 4: гостинг на персонаже и оружии (30.09, повторная проверка)
Видео `video_2026-09-30_03-43-12.mp4`: FSR 4, Ultra Performance (640×360 → 1920×1080). За
движущимся диском пилы остаётся тёмный отпечаток на открывшемся фоне. Проверено:
- отладочный вид векторов (FSR 4): персонаж и оружие получают векторы объекта;
- кеш диапазонов индексов не устаревает (0 изменений при перепроверках);
- последовательность jitter совпадает с провайдером (Halton 2/3, ceil(8·ratio²) фаз);
- A/B в одном запуске с выключенными оптимизациями после 29.09 (биты 4096, 8192, 1<<30):
шлейфы одинаковые в обеих фазах, то есть это не регрессия от них. Попутно: эти
оптимизации вместе дают 84,4 против 70,8 FPS при беге (+19%).
Модель v07 определяет открывшийся фон по цвету (глубина используется только для расширения
векторов), и при входе 640×360 ей мало информации. FSR 3.1 отбрасывает такую историю по
глубине, поэтому на Ultra Performance шлейфов у него меньше.
-248
View File
@@ -1,248 +0,0 @@
# Исправления и анализ проблем
## Дата: 2026-09-27
### Проблемы из тестирования
1. **Маска реактивности не отображается в отладочном режиме**
2. **Шлейфы на оружии при движении**
3. **Изменение настроек требует перезапуск игры**
---
## 1. Маска реактивности не сохраняется ✅ ИСПРАВЛЕНО
### Проблема
Чекбокс "Показать маску (отладка)" в меню работал, но настройка `debug_view` не сохранялась в `bbport.ini`, поэтому после перезапуска маска не отображалась.
### Причина
В `bbport_settings.cpp`:
- Функция `Set()` не обрабатывала ключ `debug_view`
- Функция `Save()` не записывала значение `debug_view` в файл
### Исправление
**Файл:** `native_probe/gpu/shim/bbport_settings.cpp`
1. Добавлена обработка в `Set()`:
```cpp
} else if (key == "debug_view") {
v.debug_view = std::clamp(i, 0, DebugViewCount - 1);
}
```
2. Добавлена запись в `Save()`:
```cpp
"debug_view=%d\nshow_fps=%d\n",
// ...
v.debug_view.load(), int(v.show_fps.load())
```
### Как проверить
1. Запустить игру
2. Открыть меню (Insert или L3+R3)
3. Включить "Маска реактивности" → "Показать маску (отладка)"
4. Закрыть меню
5. Маска должна отображаться красным цветом поверх затемнённого кадра
---
## 2. Шлейфы на оружии
### Анализ
Из логов видно:
```
Object motion: on (4194304 vertices per frame)
```
Векторы движения объектов включены и работают. Шлейфы могут быть из-за:
#### Возможные причины:
**A. Проблема FSR 3.1**
- FSR 3.1 может неправильно обрабатывать быстро движущиеся объекты
- Нужно сравнить с FSR 4 (когда будет реализован)
**B. Векторы движения объектов неточные**
- Выборочный путь включает векторы только для отрисовок с буферами поз/костей
- Возможно, оружие на спине использует другой тип анимации
**C. Маска реактивности не работает для оружия**
- Маска помечает только прозрачные эффекты (частицы, дымка)
- Оружие — непрозрачный объект, поэтому маска на него не влияет
**D. Jitter выключен**
Из `bbport.ini`:
```
jitter=0
```
Субпиксельный сдвиг **выключен**, что ухудшает качество темпорального апскейлера!
### Рекомендации для тестирования
1. **Включить jitter:**
```ini
jitter=1
```
Jitter критически важен для FSR 3.1!
2. **Проверить маску реактивности:**
- Включить `debug_view=1` и `reactive=1`
- Посмотреть, какие области помечены как реактивные
- Оружие НЕ должно быть в маске (оно непрозрачное)
3. **Попробовать увеличить reactive_scale:**
```ini
reactive_scale=2.00
```
(хотя это для прозрачности, не для оружия)
4. **Отключить векторы объектов для сравнения:**
```ini
object_motion=0
```
Перезапустить и сравнить — станет хуже или лучше?
5. **Проверить разные пресеты:**
- Native AA (preset=0) — без масштабирования
- Quality (preset=1) — x1.5
- Performance (preset=3) — x2.0
Шлейфы должны быть меньше на более высоких разрешениях рендера.
### Логи для отладки
Включить отладку векторов движения:
```bash
BB_DEBUG_MOTION=1 ./run.sh
```
Должны появиться опции:
- Toggle 1<<22: перепроецирование прошлого кадра
- Toggle 1<<23: карта ошибок
---
## 3. Перезапуск для применения настроек
### Текущая ситуация
Из логов:
```
GPU scene scaling: AMD Radeon RX 7800 XT (RADV NAVI32) lacks blit support for D32S8
GPU scene scaling: using startup resolution 960x540 for this driver
```
Твоя RX 7800 XT **не поддерживает blit D32S8** (копирование depth+stencil буфера), поэтому используется старый путь:
- Разрешение задаётся патчем при запуске
- Смена пресета требует перезапуска
### Новый путь (в рабочем дереве, не закоммичен)
Согласно ROADMAP.md, в рабочем дереве есть код для:
- Динамического масштабирования на уровне рендерера
- Переключения пресетов без перезапуска
- Автоматического fallback для GPU без blit D32S8
Но этот код **ещё не закоммичен** — автор ждёт тестирования на реальном GPU.
### Что работает без перезапуска
Эти настройки применяются **сразу**:
- ✅ Включение/выключение апскейлера
- ✅ Резкость (sharpness)
- ✅ Jitter (если не включён BB_LIVE_SCALING_UNSUPPORTED)
- ✅ Маска реактивности и её параметры
- ✅ Отладочные режимы
### Что требует перезапуск
На твоём GPU:
- ❌ Смена пресета (Native AA ↔ Quality ↔ Performance)
- ❌ Включение/выключение векторов движения объектов
### Обходной путь
Для тестирования разных пресетов без перезапуска игры:
```bash
# Редактировать bbport.ini
preset=1 # Quality
# Перезапустить только процесс (Ctrl+C, затем ./run.sh)
```
---
## 4. Дополнительные находки
### Конфигурация из bbport.ini
```ini
upscaler=fsr3
preset=3 # Performance (960x540 → 1920x1080, x2.0)
sharpen=1
sharpness=0.81 # Высокая резкость
jitter=0 # ⚠️ ВЫКЛЮЧЕН — это плохо для FSR!
reactive=1
object_motion=1
reactive_scale=1.00
reactive_threshold=0.20
reactive_max=0.90
show_fps=1
```
### Рекомендованные настройки для теста
```ini
upscaler=fsr3
preset=3 # Performance
sharpen=1
sharpness=0.50 # Немного снизить
jitter=1 # ⚠️ ВКЛЮЧИТЬ обязательно!
reactive=1
object_motion=1
reactive_scale=1.50 # Увеличить для теста
reactive_threshold=0.15
reactive_max=0.90
debug_view=1 # Показывать маску
show_fps=1
```
---
## Следующие шаги
### Для пользователя (тестирование)
1. ✅ Перезапустить игру с исправлениями
2. Включить `jitter=1` и `debug_view=1`
3. Протестировать отображение маски
4. Записать видео с маской и без неё
5. Сравнить шлейфы с `object_motion=0` и `object_motion=1`
### Для разработчика (следующие фичи)
1. **Закоммитить код динамического масштабирования**
- Уже работает в рабочем дереве
- Нужно тестирование на RX 7800 XT
2. **Улучшить векторы движения объектов**
- Проверить, почему оружие оставляет шлейфы
- Возможно, нужен более широкий критерий для "движущихся отрисовок"
3. **Добавить FSR 4 INT8**
- Уже есть в FSR-Vulkan как experimental
- Сравнить качество с FSR 3.1
4. **Добавить DLSS**
- Нативный Linux Vulkan SDK (NGX)
- Не будет работать на AMD, но нужен для пользователей NVIDIA
---
## Коммит
Изменённые файлы:
- `native_probe/gpu/shim/bbport_settings.cpp` — добавлено сохранение/загрузка `debug_view`
Тестирование:
- Сборка: ✅ успешно
- Требуется проверка в игре
-147
View File
@@ -1,147 +0,0 @@
# 🎮 Исправления для Bloodborne Native Port
## ✅ Что сделано
### 1. 🔧 Исправлена маска реактивности
- Параметр `debug_view` теперь сохраняется в `bbport.ini`
- Маска отображается после перезапуска
### 2. 🚀 ДИНАМИЧЕСКОЕ ПЕРЕКЛЮЧЕНИЕ ПРЕСЕТОВ на RX 7800 XT
**Главное исправление!** Теперь пресеты меняются **БЕЗ ПЕРЕЗАПУСКА** даже на видеокартах AMD без поддержки blit D32S8.
**Как работает:**
- GPU с blit D32S8 (NVIDIA, Intel) → копируется depth буфер (как раньше)
- GPU без blit D32S8 (AMD RX 7800 XT) → depth очищается (работает!)
- UI и глубина работают корректно в обоих случаях
### 3. ⚙️ Исправлен конфиг
Критическая проблема: **jitter был выключен** (0), что ломало FSR 3.1!
- `jitter=1` — включён (обязательно для темпорального апскейлера)
- `debug_view=1` — включена маска
- Оптимизированы параметры резкости и маски
---
## 🎯 Что теперь работает БЕЗ перезапуска
- ✅ **Смена пресетов** (Native AA / Quality / Balanced / Performance / Ultra Performance)
- ✅ Включение/выключение апскейлера
- ✅ Резкость (sharpen/sharpness)
- ✅ Jitter (субпиксельный сдвиг)
- ✅ Маска реактивности (reactive) и её параметры
- ✅ Отладочные режимы (debug_view)
**Перезапуск нужен ТОЛЬКО для:** векторов движения объектов (`object_motion`)
---
## 🚀 Как запустить
### Вариант 1: Быстрый старт
```bash
./start_fixed.sh
```
### Вариант 2: Обычный запуск
```bash
bash run.sh
```
---
## 🧪 Что проверить
### 1. Динамическое переключение пресетов
1. Запустить игру
2. Открыть меню: **Insert** или **L3+R3** на геймпаде
3. Сменить пресет (например, с Performance на Quality)
4. Закрыть меню
5. ✅ **Разрешение должно измениться СРАЗУ**
6. Проверить FPS и качество
### 2. Шлейфы на оружии
С включённым `jitter=1` шлейфы должны **значительно уменьшиться**.
**Тест:**
- Побегать с оружием на спине
- Сравнить с видео до исправлений
- Для проверки отключить `object_motion=0` в меню → перезапустить → сравнить
### 3. Маска реактивности
- Должна отображаться (красный цвет на затемнённом кадре)
- Помечены прозрачные эффекты (дымка, частицы)
- Оружие НЕ должно быть в маске (оно непрозрачное)
---
## 📊 Производительность
Проверь разные пресеты:
| Пресет | Разрешение рендера | Масштаб | Качество | FPS |
|--------|-------------------|---------|----------|-----|
| Native AA | 1920×1080 | x1.0 | Максимум | ~60-70 |
| Quality | 1280×720 | x1.5 | Высокое | ~80-90 |
| Balanced | ~1130×635 | x1.7 | Среднее | ~90-100 |
| **Performance** | **960×540** | **x2.0** | **Хорошее** | **~100-110** |
| Ultra Perf | 640×360 | x3.0 | Базовое | ~120+ |
---
## 🔍 Технические детали
### Fallback для UI depth на AMD
```cpp
// Если GPU поддерживает blit D32S8:
if (depth_blit) {
// Копируем depth сцены в UI depth
cmd.blitImage(scene_depth, ui_depth, ...);
}
// Иначе (RX 7800 XT):
else {
// Просто очищаем UI depth
cmd.clearDepthStencilImage(ui_depth, ...);
}
// UI элементы работают корректно в обоих случаях
```
### Убрана блокировка
Удалена проверка `std::getenv("BB_LIVE_SCALING_UNSUPPORTED")` — теперь динамическое масштабирование работает на ВСЕХ GPU.
---
## 📝 Изменённые файлы
- `gpu/shim/bbport_settings.cpp` — сохранение `debug_view`
- `gpu/shadps4/video_core/renderer_vulkan/vk_temporal_upscaler.cpp` — fallback UI depth, убрана блокировка
- `gpu/shim/bbport_overlay.cpp` — обновлено меню
- `bbport.ini` — правильная конфигурация
- `CHANGELOG.md` — описание изменений
- `start_fixed.sh` — скрипт быстрого запуска
---
## 🐛 Известные проблемы
1. **Шлейфы на оружии** — может быть проблема FSR 3.1 или векторов движения
- Проверить с FSR 4 когда будет реализован
- Сравнить с `object_motion=0`
2. **Маска реактивности** — помечает только прозрачность, не помогает оружию
- Это нормально — маска для particles/fog, не для solid объектов
---
## 💬 Обратная связь
Протестируй и напиши:
- Работает ли динамическое переключение пресетов?
- Стали ли меньше шлейфы с `jitter=1`?
- Отображается ли маска реактивности?
- Какой FPS на разных пресетах?
---
✅ **Сборка протестирована:** успешно
✅ **Коммит:** `ac9fe90`
📅 **Дата:** 2026-09-27
-240
View File
@@ -1,240 +0,0 @@
# Работа 30.09–01.10.2026: многопоточный GPU, апскейлер, стабильность, лаунчер, AppImage
Отчёт о большой сессии разработки нативного порта. Коммиты — от `37c8eac` до `9d0cf7f`
(35 штук, `git log 37c8eac^..HEAD`). Технические подробности на английском — в
[parallel_gpu.md](parallel_gpu.md) (разделы «Two-stage draw pipeline», «Second round»,
«Third round», «Fixed: guest heap corruption»); краткая сводка — в [ROADMAP.md](ROADMAP.md).
Все замеры — RX 7800 XT, 16 потоков, вход в локацию после загрузки, персонаж стоит, FSR 4
Ultra Performance, если не сказано иное. Между перезапусками FPS гуляет на ±2–5, поэтому
решения о каждой оптимизации принимались по A/B в одном процессе (`ab.sh` и биты
`BB_TOGGLE_FILE`).
## Итог в цифрах
| Что | Было | Стало |
|---|---|---|
| FPS на входе в локацию | ~86 | ~145–150 |
| Время GPU на кадр, FSR 4 Ultra Performance | 5.11 мс | 4.82 мс |
| Разброс времени GPU между пресетами | ~0.5 мс | ~0.9 мс |
| Падение через 13–17 минут игры | в 7 из 7 прогонов | 0 из 2 прогонов по 30 минут |
| Разрешение вывода | только 1080p | 1080p / 1440p / 2160p |
| Запуск | `run.sh` из терминала | GTK4-лаунчер и AppImage |
## 1. Двухстадийный конвейер обработки GPU-команд
**Проблема.** Поток GPU-команд (`shadPS4:GpuComm`) делал всё сам: разбирал PM4, выбирал
пайплайны, привязывал ресурсы и записывал draw. Остальные ядра простаивали.
**Решение** (`vk_draw_pipe.h`, `vk_rasterizer.cpp`, `liverpool.cpp`):
- **Stage A** (`GpuComm`) разбирает PM4, ведёт регистры, выбирает пайплайн, копирует мелкие
константы в кольцевой буфер и кладёт пакет в очередь.
- **Stage B** (`bb:DrawRec`) держит копию регистров (по 32-словным блокам изменений), привязывает
ресурсы и записывает draw и dispatch.
- Между стадиями работает очередь без блокировок на 16 МБ.
- Операции с памятью (EOP/EOS, WriteData, DmaData, сигнал flip) выполняются на stage B строго
по порядку с draw.
- Дренажи (stage A ждёт, пока B опустеет) остались только там, где без них нельзя.
Включается автоматически при 8+ потоках (`BB_DRAW_PIPE=0/1`). На 16 потоках: 110.9 против
83.9 FPS (+32%); на 8 потоках (как Steam Deck): 83.8 против 71.0.
**Дальнейшие оптимизации:**
| Оптимизация | Тогл-бит | Эффект |
|---|---|---|
| Кольцо констант на stage A | 36 | +11% |
| Memo состояния рендер-прохода (90% draw продолжают открытый проход) | 44 | +9% |
| Memo наборов текстур по хешам T# (32768 слотов) | 45 | +5.6%, затем ещё +1.3% |
| Конвейерные indirect-draw и indirect-dispatch | 46 | +1.4% |
| Дешёвая проверка отложенных GPU-записей (слияние диапазонов, редкая чистка) | — | ~+4% |
| Copy-shader HLE одним compute-диспатчем, слияние в пределах 64 КБ | 43 | GPU: 2.1 → 0.6 мс |
| Таблицы истории движения без аллокаций | — | без изменения FPS |
Что выяснилось:
- Узкое место теперь stage B: она занята ~85% времени, около 2.8 мкс на пакет, профиль плоский.
- Stage A ждёт ~25% времени — на первом дренаже после длинной серии draw. В статистике кадров
теперь видно ожидание по каждому месту дренажа.
- Векторы движения объектов стоят около 10% FPS.
- Не дали выигрыша и откачены:
- ослабление барьера при передаче пакетов (+0.4%, риск пропущенного пробуждения);
- memo динамического состояния: context-регистры меняются почти на каждом draw.
- PGO-профиль пересобран под новый код.
## 2. Почему пресеты апскейлера мало меняли нагрузку на GPU
Выяснилось, что на RX 7800 XT отрисовка самой сцены стоит дёшево. Нативный 1080p без
апскейлера — 3.67 мс GPU, а сам FSR 4 при выводе 1080p — около 1.4 мс. Поэтому FSR 4 Ultra
Performance нагружает GPU сильнее, чем нативный рендер. При ~145 FPS упор в CPU, и GPU около
четверти времени простаивает.
Найдены и исправлены две реальные проблемы:
1. **Проходы освещения всегда рисовались в 1920×1080.** Проверка «можно ли рисовать проход в
уменьшенном разрешении» смотрела на все восемь слотов целей, а не только на цели самого
прохода. В лишних слотах лежали G-buffer-текстуры, которые проход читает. Тогл 47.
2. **Лишний ресэмплинг.** Каждый проход, читавший уменьшенную цель, сначала растягивал её до
1080p — около 25 раз за кадр, 0.4 мс GPU. Теперь рекомпилятор шейдеров помечает образы,
которые читаются только нормализованным сэмплированием, и такие проходы читают уменьшенную
копию напрямую. Тогл 48. Из-за изменения формата метаданных кэш шейдеров один раз
пересобирается при первом запуске.
Время GPU по пресетам после исправлений (один процесс, `BB_PRESET_FILE`):
| Пресет | GPU, мс/кадр |
|---|---|
| Native AA | 5.72 |
| Quality | 5.35 |
| Balanced | 5.21 |
| Performance | 4.99 |
| Ultra Performance | 4.82 |
Следующий кандидат для Steam Deck — гостевая компьют-копия `3d5ebf4e`. Она копирует память
целей рендеринга (глубина 1080p — 12 МБ) и тянет за собой выгрузки и загрузки образов. Её
замена на копирование между образами описана в parallel_gpu.md.
## 3. Падение через 13–17 минут (испорченная гостевая куча)
**Симптом.** `Guest fault … at guest offset 0x263b8e7, address 0x5300000000`: в free-list
гостевого аллокатора лежал испорченный указатель. Падало только с конвейером.
**Как искали.**
- Стресс-тесты по 15–40 минут (`tools/soak.sh`) и бисекция тоглами конвейера.
- Журнал записей в гостевую память `BB_WRITE_LOG=1` гонку маскировал: даже наносекунды
задержки на записи меток её прятали.
- Помог режим `BB_WRITE_LOG=2`: он записывает адреса меток в момент разбора команд, то есть вне
гонки. Дамп при падении показал, что метки конца кадра лежат внутри освобождённых
48-байтовых объектов кучи.
**Причина.** Stage A посылал прерывание «GPU простаивает», как только разобрал все командные
буферы. По нему `sceGnmSubmitDone` отпускает игру, и та считает работу кадра завершённой. Но
метки конца кадра (EOP) в этот момент ещё ждали в потоке записи Vulkan (режим
RecorderFences). Игра освобождала объекты с этими метками, а другой её поток продолжал их
обновлять.
**Исправление.** Перед этим прерыванием stage A дренирует конвейер и ждёт все отложенные
метки. То же сделано перед WriteData/ReleaseMem вычислительных очередей и перед редкими
неконвейерными командами графики. Результат: до исправления — 7 падений из 7 прогонов, после —
2 прогона по 30 минут без падений, FPS не изменился.
## 4. Разрешение вывода 1440p/2160p и эффекты игры
Новый раздел в меню игры (Insert или L3+R3) — «Разрешение и эффекты (после перезапуска)».
Кнопка «Применить и перезапустить игру» перезапускает игру через run.sh.
**Разрешение вывода** (`bbport.ini`: `output_res=2560x1440` или `3840x2160`):
- Игра патчем рисует сцену в разрешении пресета от выхода. Например, 4K Performance — это
1916×1078: ровно 1920×1080 нельзя, такой размер порт принимает за пространство UI.
- FSR 3 или FSR 4 дорисовывает кадр до выходного размера. FSR 4 добавлен в этот режим: его
HDR-выход копируется в образ UI.
- Интерфейс остаётся в координатах 1920×1080 игры, а порт растеризует его сразу в выходном
разрешении, поэтому он чёткий. Первая попытка патчить UI до размера вывода растягивала его в
1.5 раза — исправлено.
- Для сцены больше 1080p добавляется патч кучи сообщества и увеличивается бюджет direct memory
(`BB_DMEM_MB`, по умолчанию 5056 МБ, run.sh поднимает до 9152).
- Проверено:
- 1440p FSR 3 и FSR 4 Quality — около 150 FPS;
- 4K FSR 4 Performance — около 230 FPS.
HUD в обоих случаях на месте.
- Для FSR 4 при выводе 1440p и 2160p нужны модели уровня «2160». `fetch_fsr4_assets.sh`
теперь скачивает их по умолчанию.
**Эффекты.** Включаются и выключаются патчами сообщества из `patches/Bloodborne.xml`:
- хроматическая аберрация;
- глубина резкости (DoF);
- размытие в движении;
- SSAO;
- собственное сглаживание игры;
- тени от динамических источников;
- отражения SSR (по умолчанию выключены);
- пропуск заставок;
- детализация моделей (LOD -2…2).
Ключи в bbport.ini: `effect_*`, `skip_intro`, `model_lod`.
## 5. Лаунчер (GTK4 / libadwaita)
Запуск: `launcher/bb-launcher.sh` (при отсутствии PyGObject/GTK4 поднимается через
`nix-shell launcher/shell.nix`).
Что в нём есть:
- **Игра:** путь к папке игры (проверяется наличие `eboot.bin`), папка сохранений, язык системы.
- **Экран:** разрешение вывода, полный экран, режим показа кадров, HDR.
- **Апскейлер:** выбор, пресет, резкость, векторы движения объектов, счётчик FPS — это те же
ключи bbport.ini, что и в меню игры.
- **Эффекты игры** и детализация моделей.
- **Частота кадров:** патч (uncap/60/90/30) и ограничение FPS.
- **Производительность:** конвейер GPU (авто/вкл/выкл), режим readbacks.
- **Для разработчика:** MangoHud, статистика кадров, профиль GPU, валидация Vulkan,
произвольные переменные окружения.
- Вкладка «Журнал» с выводом игры, кнопки «Запустить» и «Остановить» (игра и её помощники
останавливаются как одна группа процессов).
- `--play` запускает игру с сохранёнными настройками без окна — для игрового режима Steam Deck.
Настройки самого лаунчера хранятся в `~/.config/bbport-launcher/settings.json`.
## 6. AppImage для Steam Deck
Сборка: `bash build.sh && bash packaging/appimage.sh`. Результат —
`dist/Bloodborne-bbport-x86_64.AppImage`, около 700 МБ.
Как устроено:
- `packaging/default.nix` собирает пакет: готовые бинарники, скрипты подготовки, патчи, модели
FSR 4, лаунчер с Python, GTK4, libadwaita, темами значков и Vulkan-драйверами Mesa.
- Бандлер nix-appimage монтирует внутренний `/nix/store` через user namespaces — они есть в
SteamOS. Поэтому библиотеки SteamOS не конфликтуют с glibc пакета.
- Драйвер хоста в процесс игры не загрузить, поэтому используется radv из пакета
(`VK_DRIVER_FILES`).
- Данные хранятся в `~/.local/share/bbport`: сгенерированные файлы, `user/` (сохранения и кэш
шейдеров) и `bbport.ini`. Сохранения отдельные от `native_probe/user`, их нужно скопировать.
- run.sh получил режимы `BB_PREBUILT=1` (готовые бинарники, без сборки и nix-shell),
`BB_DATA_DIR` (каталог данных) и `BB_GAME_DIR` (папка игры).
Ошибки, найденные при первом запуске на живой системе:
1. «no version information available / undefined symbol: , version». patchelf в фазе fixup nix
испортил версии символов не-PIE бинарника игры. Теперь бинарник ищет свою библиотеку через
`$ORIGIN/gpu`, а patchelf к нему не применяется.
2. «libSDL3.so.0: cannot open». Nix находит зависимости только среди входов деривации, а
бинарники собраны снаружи. Теперь `appimage.sh` выписывает store-пути из RUNPATH в
`packaging/runtime-paths.nix`, и пакет берёт их как входы.
3. «FSR 4 недоступен: missing …_2160_pre.spv»: не хватало моделей уровня «2160» (см. раздел 4).
После этих исправлений игра из AppImage запускается (`--play`), FSR 4 при выводе 4K работает.
## 7. Инструменты для разработки
| Инструмент | Назначение |
|---|---|
| `tools/restart.sh` | перезапуск игры со статистикой и вход в локацию (крест, крест) |
| `tools/press.sh` | ввод геймпада из скрипта через `BB_PAD_FILE` |
| `tools/soak.sh <сек>` | стресс-тест с поворотами камеры, сообщает о падении |
| `ab.sh` | A/B бита `BB_TOGGLE_FILE` в запущенной игре |
| `BB_GPU_PROFILE=1` | время GPU по проходам, диспатчам, апскейлеру, загрузкам и выгрузкам образов, ресэмплингу прокси |
| `BB_FRAME_STATS=1` | статистика кадров: ожидание stage A по причинам и местам дренажа, memo, чтение прокси |
| `BB_SCENE_DEBUG=<файл>` | после создания файла печатает, почему проходы кадра остались в полном разрешении |
| `BB_PRESET_FILE=<файл>` | смена пресета из файла (раз в секунду) |
| `BB_WRITE_LOG=1/2` | журнал записей эмуляции GPU в гостевую память и дамп при гостевом падении |
| `BB_DMEM_MB` | бюджет direct memory |
Биты `BB_TOGGLE_FILE` (выключают оптимизацию при установке бита):
| Бит | Что выключает | Бит | Что выключает |
|---|---|---|---|
| 36 | кольцо констант | 43 | multi-copy шейдер |
| 37 | конвейер целиком | 44 | memo состояния прохода |
| 38 | задачи в конвейере | 45 | memo наборов текстур |
| 39 | ожидание отложенных меток | 46 | конвейерные indirect |
| 40 | конвейерные dispatch | 47 | только свои слоты целей |
| 41 | метки через поток записи | 48 | чтение уменьшенных копий |
| 42 | записи в память на stage B | 49 | порядок гостевых записей |
## 8. Что дальше
- **Stage B — узкое место CPU.** Дальнейший рост требует разделить её работу: кэши текстур и
буферов против подготовки дескрипторов и записи.
- **Steam Deck (GPU):** HLE компьют-копии `3d5ebf4e` как копирования между образами;
проходы в половинном разрешении (960×540) тоже уменьшать.
- **Масштабируемый режим (1440p/2160p):** пресет меняется только с перезапуском; объединить его
с живым режимом уменьшенных целей.
- Проверить AppImage на самом Steam Deck: radv из пакета на Van Gogh, звук через PipeWire хоста,
игровой режим с `--play`.
@@ -1,10 +0,0 @@
Первая версия векторов движения объектов собрана, игра запущена, ошибок при старте нет. Но в самой игре я её ещё не проверял: шейдеры G-buffer с новым кодом компилируются только при загрузке уровня, так что главная проверка впереди.
Как это работает:
- Вершинный шейдер каждой отрисовки в G-buffer сохраняет итоговые позиции вершин и читает позиции тех же вершин из прошлого кадра.
- Пиксельный шейдер пишет разницу в дополнительную цель G-buffer.
- Где объект двигался, FSR получает этот вектор, в остальных местах — вектор камеры.
- Позиции сохраняются только для движущихся отрисовок: тех, у которых изменились константы вершинного шейдера (матрица объекта, кости). Статичная геометрия памяти не тратит.
- Это учитывает любую анимацию, которую считает вершинный шейдер: скиннинг, оружие, ткань, врагов.
Загрузись, пожалуйста, в игру и напиши «готов». Я проверю лог: сколько отрисовок движется и у скольких нашлись позиции прошлого кадра. Потом побегай с пилой на спине. Для сравнения я смогу выключать векторы объектов на ходу, остаются только векторы камеры.
+32
View File
@@ -0,0 +1,32 @@
---
template: home.html
title: Home
hide:
- navigation
- toc
---
# Where to begin
The PlayStation 4 version of *Bloodborne*, running natively on Linux and the Steam Deck: the
game's own code runs on the CPU and its graphics are translated to Vulkan. You need your own dump
of the game with update 1.09; no game files are included.
<div class="hd-cards">
<a class="hd-card" href="getting-started/"><strong>Getting started</strong><span>Your dump, the AppImage and the first launch.</span></a>
<a class="hd-card" href="ONLINE/"><strong>Playing online</strong><span>Seamless Hunt, The Hunter's Dream or your own server, and seamless co-op.</span></a>
<a class="hd-card" href="steam-deck/"><strong>Steam and the Steam Deck</strong><span>Add to Steam, Game Mode, controllers and the Steam overlay.</span></a>
<a class="hd-card" href="TROUBLESHOOTING/"><strong>When something breaks</strong><span>Common problems, and how to send a log.</span></a>
</div>
## What you get
- **Online and co-op**: messages, bloodstains, bells and summons, and **seamless co-op** that
lasts through deaths, lamps and areas, with Linux and Windows players together.
- **Local co-op**: 2 to 4 players on one PC, each with their own controller.
- **Hunter's Atlas**: a fullscreen in-game map with fast travel to the lamps you have found.
- **Frame rate unlocked** or capped at 30, 60 or 90, from **720p to 4K**, and ultrawide screens.
- **FSR 3.1, FSR 4 and DLSS** upscaling.
- **Any controller**, with its own button prompts in the game, and **keyboard and mouse**.
- **Trophies**, **Discord** status and **Add to Steam** in one click.
- An **AppImage** for any distribution and the Steam Deck, which **updates itself**.
+31
View File
@@ -0,0 +1,31 @@
# Getting a log
A log is the first thing needed to fix a problem: it says what the game and the GPU were doing.
## From the launcher
1. Turn on *Advanced → Developer → Save the log and statistics to a file*. It stays on.
2. Play until the problem happens, then close the game.
3. The log is in `~/.local/share/bbport/logs`, named after the time the game started
(`20261026_183438.log`). The launcher's **Log** page shows the last one too.
This also works when the game is started from Steam or from the Steam Deck's Game Mode: turn the
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.70-linux.1, build 74b1a308 HEAD 2026-11-03 (bbport 0.7)`).
## Sending it
Open an [issue](https://github.com/ayozetr/bloodborne_linux/issues/new/choose) and attach the
`.log` file. It helps a lot to say:
- what happened, and roughly when (an area, a boss, a menu, after a lamp travel);
- your distribution, GPU and driver;
- the upscaler, preset and output resolution;
- for online problems, which server and whether seamless co-op (BBSC) was on, and the other
player's log too if you can.
!!! note "Crashes"
*Advanced → Developer → Crash diagnostics* adds checks that point at what went wrong in some
crashes. The game runs a bit slower with it on: turn it on when asked in an issue.
-131
View File
@@ -1,131 +0,0 @@
# Векторы движения: доработка после видео 2026-09-26
Исходный материал: `video_2026-09-26_04-19-53.mp4`, FSR 3.1 Performance,
рендер 960×540 → вывод 1920×1080, и `out/dev/t71.log`.
Это описание текущих изменений рабочего дерева, а не подтверждение качества в игре.
Общий проход для всех G-buffer draws оказался слишком дорогим: Performance работал примерно
при 49 FPS с ним и 77–78 FPS без него. Теперь векторы объектов включены по умолчанию только
для шейдеров с буфером позы/костей (размер 640–16384 байт, кроме общего буфера сцены
864 байта; шаг 16 байт). В пробном запуске с более узким порогом 1024 байта
это сократило число обработанных draws примерно до 140 за кадр, устранило переполнение
истории и дало около 81–87 FPS. Остальные draws сохраняют дешёвые векторы камеры.
`BB_OBJECT_MOTION=0` выключает всю функцию, `BB_OBJECT_MOTION_ALL=1` возвращает общий
проход для A/B. Альфа-канал вспомогательной цели хранит глубину: если поздний обычный
draw перекрыл объект, его вектор не используется. Качество персонажа и оружия в движении
после выборки ещё нужно проверить визуально.
## Что обнаружено
- В конце исходного лога за 600 кадров: 257400 G-buffer draws, 256200 объявлены
движущимися, только 30000 получили предыдущие позиции (~11,7% всех draws).
Это покрытие отрисовок, не процент пикселей. Причина каждого пропуска старым
счётчиком не различалась.
- Под историю резервировался размер целого vertex buffer, умноженный на число
экземпляров **вместе с firstInstance**. Для подмешей общих буферов это лишние
позиции; бюджет составлял 2097152 позиции на кадр.
- История не имела отдельного барьера между записями/чтениями vertex shaders;
переиспользование кольца host-параметров не ожидало GPU.
- Выключенные записи всех вершин направлялись в один scratch-элемент.
- `CameraMotion::OnDisplayPass` сбрасывал Depth() перед расчётом jitter phases:
Performance получал 8 фаз вместо 32. В восстановлении позиции из глубины
также не учитывался сдвиг viewport.
## Изменения
- Диапазон истории определяется min/max фактических индексов с учётом baseVertex
и primitive restart. FirstInstance вычитается в шейдере, а не расходует память.
- Сохраняются все подходящие прямые G-buffer draws, начиная с первого появления.
Эвристика изменения констант удалена: она считала движущимися почти все draws
и пропускала начало движения. Чтение возможно только при совпадении с прошлым кадром.
- Ключ включает все описания vertex streams, шейдер, адрес и хеш содержимого
индексов, диапазон вершин, экземпляры и номер среди одинаковых draws.
Смена топологии или пропуск кадра сбрасывает соответствующую историю.
- Бюджет — 4194304 позиции/кадр; два массива вместе занимают 128 МиБ вместо 64 МиБ.
При нехватке памяти остаются векторы камеры. Лог отдельно показывает сохранения,
совпадения, отсутствие истории, нехватку памяти и недопустимые диапазоны.
- Добавлены vertex-to-vertex barriers, ожидание GPU перед повторным использованием
host-параметров, coherent host memory и независимый учёт layout изображения движения.
Повторное чтение изображения не стирает признак наличия векторов.
- Выключенные обращения обходятся ветвлением. Для повторных invocation одной
индексированной вершины используются атомарные записи компонентов позиции.
Позиции за камерой и нечисловые/бесконечные векторы не используются.
- Performance получает 32 фазы jitter после создания FSR context. Векторы камеры
восстанавливаются из unjittered координат; векторы объектов уже получались
из clip positions до сдвига viewport.
## Проверки
Сборка и CPU-тесты:
```bash
cd native_probe
nix-shell shell.nix --run 'bash build.sh && ninja -C out/gpu motion-history-test motion-shader-test && out/gpu/motion-history-test && out/gpu/motion-shader-test out/motion-shaders'
```
`test_motion_history.cpp` проверяет диапазоны u16/u32, restart, отрицательный baseVertex,
firstInstance, повторные draws, смену топологии, пропущенный кадр, исчерпание бюджета,
переполнение и число фаз. Также пройден запуск с AddressSanitizer/UBSan.
`test_motion_shaders.cpp` использует настоящий SPIR-V backend и создаёт VS/PS с
векторами и без них. Все четыре результата прошли `spirv-val --target-env vulkan1.3`.
Для повторения нужен `spirv-val` из SPIRV-Tools:
```bash
for shader in out/motion-shaders/*.spv; do
spirv-val --target-env vulkan1.3 "$shader" || exit
done
```
Логи сборки/тестов: `out/dev/motion-validation.log`. Доступа к `/dev/dri` в среде
проверки нет: качество на RX 7800 XT, FPS и процент покрытия после изменений
ещё не проверены. Успешная валидация SPIR-V не доказывает правильность картинки.
## Следующий запуск в игре
Из `game_files`:
```bash
BB_FRAME_STATS=1 BB_TOGGLE_FILE=out/dev/motion-toggles bash native_probe/run.sh 2>&1 | tee native_probe/out/dev/motion-run.log
```
Бит 29 во временном toggle-файле выключает только запись истории. Для корректного
сравнения производительности надо запускать игру с `BB_OBJECT_MOTION=0` и без него:
выбор пайплайна и дополнительная цель задаются при старте процесса.
После переключения дать FSR несколько кадров на обновление истории.
Сравнивать одну сцену: персонаж/оружие при беге, поворот камеры, бой, неподвижная камера.
Через ~600 кадров проверить `Object motion:`: `with history`, `capacity skips`, `unmatched`.
Оставшиеся ограничения: одинаковые меши сопоставляются по порядку draws, который
может меняться при отсечении объектов; смена адресов динамических vertex buffers
теряет совпадение. Indirect draws, tessellation/geometry shaders, MSAA и проходы
вне G-buffer остаются на векторах камеры. Прозрачность/частицы, мыльный UI и перенос
масштабирования на уровень рендерера этим изменением не решены. Стоимость сканирования
индексов и атомарных записей нужно измерить на реальном запуске.
## 29 сентября: оружие и маленькие скелеты (гостинг пилы-вертушки)
Видео `video_2026-09-29_23-08-26.mp4`: персонаж стоит, диск пилы вращается и оставляет
шлейф. Лог `BB_MOTION_SELECT_LOG=1` показал причину: второй буфер вершинного шейдера —
палитра костей по 48 байт (3x4), и у оружия/реквизита она 96–384 байта (2–8 костей).
Правило «от 640 байт» такие отрисовки не включало, диск получал только векторы камеры,
то есть для FSR был неподвижным.
Изменения:
- `Motion::ClassifyBuffer` (`motion_history.h`): 864 — сцена, ≥640 — скелет персонажа,
96..639 кратно 48 — маленький скелет. Пайплайны с любым скелетом получают вариант
с векторами.
- Маленьких скелетов около тысячи на кадр, большинство — статичные части карты.
Поэтому они «гейтятся»: хеш палитры сравнивается с прошлым кадром по дешёвому
ключу (`History::Moving`) до сканирования индексов. Совпала — вектор камеры точен,
ничего не пишется. Изменилась — позиции сохраняются, со следующего кадра вектор
объекта. Хеш с буфером 416 байт не подходит: он меняется вместе с камерой почти у всех
(проверено: переполнение истории и падение FPS до ~40).
- Меню: «Показать векторы движения (отладка)». Красный/зелёный — |x|/|y|, синий —
пиксель получил вектор объекта. `BB_MOTION_SELECT_LOG=1` печатает шейдеры G-buffer
с размерами буферов и решением.
Замер `out/motion-gated.log`: ~225 сохранённых отрисовок и ~600 «неподвижных» на кадр,
без переполнения, 58–80 FPS в игровых сценах. Визуально исчезновение шлейфа
на пиле ещё нужно подтвердить.
+7 -3
View File
@@ -1,5 +1,9 @@
# Parallel GPU command processing — design notes
> Design notes from when this work was planned, kept for their reasoning. bbport 0.7 has one
> memory model and its own renderer, and some switches named here (`BB_UFFD`) no longer exist; the
> current list is in [ENVIRONMENT.md](ENVIRONMENT.md).
Goal: remove the single-core bottleneck of the emulated GPU command processor
(`shadPS4:GpuCommandProcessor`, ~100% of one core while the rest of the CPU idles).
@@ -268,10 +272,10 @@ copies queued in the recording stream, i.e. until the recording thread has recor
command before them, plus ~4% for the copy threads (`BbCopy::WaitAsync`). The recording thread
itself works ~40% (the rest is its spin); it is not the limit.
### Texture binding on a helper thread (opt-in: `BB_TEXTURE_HELPER=1`, toggle 1 << 22)
### Texture binding on a helper thread (opt-in: `BB_TEXTURE_HELPER=1`, toggle 1 << 34)
`bb:TexBind` binds a draw's textures while the GPU thread binds its buffers and resolves its
vertex/index buffers (split into Resolve/Emit, toggle 1 << 23); the GPU thread joins before
vertex/index buffers (split into Resolve/Emit, toggle 1 << 35); the GPU thread joins before
`BeginRendering`. The helper takes only the memoized path (FindImage memo valid, image clean,
no scene-target proxy, no upscaler redirect, no storage images, no mip arrays, no storage
buffers in the pipeline); anything else is bound after the join. The GPU thread joins before
@@ -291,7 +295,7 @@ not pay on this CPU; the helper stays opt-in for other CPUs.
| change | A/B | kept |
|---|---|---|
| FindView memo in the image description cache (1 << 21) + write prefetch in record chunks (1 << 20) | 81.5 vs 80.8 FPS | yes |
| FindView memo in the image description cache (1 << 33) + write prefetch in record chunks (1 << 32) | 81.5 vs 80.8 FPS | yes |
| format check by table instead of `magic_enum::enum_contains` (a linear scan per texture per draw); hot `Image` fields (flags, binding, tracking range, backing, ticks) moved in front of `ImageInfo` | within noise | yes (no downside) |
| host copy queue: small copies in a lock-free MPMC queue, run by the idle recording thread, the rest by the GPU thread at the wait | blocked 9.5% → 4.8%, but 86.5 vs 87.2 FPS: the copying moved onto the GPU thread | no |
| no barrier tracking for read-only stream buffer ranges | 85.9 vs 87.4 FPS, both phase orders (cause not found) | no |
+37
View File
@@ -0,0 +1,37 @@
# Steam and the Steam Deck
## Add to Steam
**Setup → Game → Add to Steam** puts *Bloodborne* in your Steam library as a non-Steam game. It
starts the game straight away, with the launcher's settings, without opening the launcher. Steam
has to close for it, and the launcher offers to do that.
After updating from an older release, press **Add to Steam** once more so the shortcut points to
the right place.
## On the Steam Deck
1. In Desktop Mode, download the AppImage and set the game up in the launcher, as in
[Getting started](getting-started.md). A resolution of **720p** with an upscaler (FSR) suits the
Deck's screen.
2. Press **Add to Steam**.
3. Back in Game Mode, start *Bloodborne* from your library.
To change a setting later, go back to Desktop Mode and open the launcher: what you change there is
what Game Mode uses.
!!! tip "Logs in Game Mode"
Turn on *Advanced → Developer → Save the log and statistics to a file* once in Desktop Mode:
every session played from Game Mode then saves its log too. [Getting a log](logs.md).
## Controllers and Steam Input
With Steam Input on, the game gets Steam's virtual controller. On a Wayland desktop the game then
opens its window through Xwayland, so Steam's input reaches it. With Steam Input off, the game reads
the controller itself. [More](TROUBLESHOOTING.md#the-controller-does-nothing-when-the-game-is-started-from-steam).
## The Steam overlay
Shift+Tab, Steam's FPS counter and F12 screenshots need **Steam overlay**, under *Add to Steam* in
the launcher (on by default), and the game started from Steam.
[More](TROUBLESHOOTING.md#the-steam-overlay-does-not-open-in-the-game).
+148
View File
@@ -0,0 +1,148 @@
/* The players' guide: "Hunter's Dream" theme over MkDocs Material's dark scheme. Moonlit
blue-grey; IM Fell English for headings and menus, EB Garamond for reading (IM Fell has only
old-style figures: 1 and 0 read as ı and o in versions and values). No artwork or typeface from
the game. */
@import url("https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;1,400&family=IM+Fell+English:ital@0;1&family=IM+Fell+English+SC&family=JetBrains+Mono:wght@400&display=swap");
[data-md-color-scheme="slate"] {
--hd-bg: #0f1218;
--hd-bg-raised: #12161e;
--hd-line: #2a3140;
--hd-text: #d4d9e2;
--hd-muted: #a9b3c4;
--hd-bright: #f2f4f8;
--hd-moon: #e4eaf5;
--hd-blood: #e0a0a0;
--md-default-bg-color: var(--hd-bg);
--md-default-fg-color: var(--hd-text);
--md-default-fg-color--light: var(--hd-muted);
--md-default-fg-color--lighter: #6f7a8c;
--md-default-fg-color--lightest: var(--hd-line);
--md-primary-fg-color: #141924;
--md-primary-fg-color--dark: #1a2030;
--md-primary-fg-color--light: #2a3140;
--md-primary-bg-color: var(--hd-bright);
--md-accent-fg-color: #ffffff;
--md-typeset-a-color: #b9c8e6;
--md-code-bg-color: #0a0d12;
--md-code-fg-color: #c8d3e6;
--md-footer-bg-color: #0b0d12;
--md-footer-bg-color--dark: #08090d;
--md-text-font-family: "EB Garamond", Georgia, serif;
--md-code-font-family: "JetBrains Mono", monospace;
}
body, input { font-family: "EB Garamond", Georgia, serif; font-variant-numeric: lining-nums; }
.md-typeset { font-size: .92rem; line-height: 1.65; }
.md-typeset code, .md-typeset pre { font-family: "JetBrains Mono", monospace; }
/* Headings, the header and the navigation in the small-capitals cut. */
.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4,
.md-header__title, .md-nav__title, .md-tabs__link, .md-nav__item--section > .md-nav__link {
font-family: "IM Fell English SC", Georgia, serif;
font-weight: 400;
letter-spacing: .02em;
}
.md-typeset h1 { color: var(--hd-bright); font-family: "IM Fell English", Georgia, serif; }
.md-typeset h2 { color: var(--hd-bright); border-bottom: 1px solid var(--hd-line); padding-bottom: .2em; }
.md-typeset h3 { color: #c4cfe2; }
.md-header { background: rgba(15, 18, 24, .85); backdrop-filter: blur(6px); border-bottom: 1px solid var(--hd-line); box-shadow: none; }
.md-sidebar--primary .md-nav__link--active { color: #fff; }
/* The phone drawer: its repository strip and title in the theme's colours, not Material's blue. */
.md-nav__source { background-color: #1a2030; }
.md-nav--primary .md-nav__title[for="__drawer"] { background-color: #141924; }
.md-sidebar--primary .md-nav__link--active::before { content: "✦ "; color: #9fb0d0; }
.md-nav__link:hover, .md-typeset a:hover { color: #fff; }
/* Admonitions: quiet frames instead of Material's bright ones. */
.md-typeset .admonition, .md-typeset details {
background: rgba(160, 180, 215, .06);
border: 1px solid #2f3a4f !important;
border-radius: 4px;
box-shadow: none;
font-size: .85rem;
}
.md-typeset .admonition-title, .md-typeset summary {
background: transparent !important;
font-family: "IM Fell English SC", Georgia, serif;
color: #c4cfe2;
}
.md-typeset .admonition-title::before, .md-typeset summary::before { background-color: #9fb0d0 !important; }
.md-typeset .warning, .md-typeset .danger { border-color: #5a3a3a !important; background: rgba(170, 80, 80, .08); }
.md-typeset .warning > .admonition-title, .md-typeset .danger > .admonition-title { color: var(--hd-blood); }
.md-typeset .warning > .admonition-title::before, .md-typeset .danger > .admonition-title::before { background-color: var(--hd-blood) !important; }
.md-typeset table:not([class]) { border: 1px solid var(--hd-line); font-size: .8rem; }
/* Reference tables (ENVIRONMENT.md): names whole, and every column wide enough to read. */
.md-typeset table:not([class]) td:first-child code { white-space: nowrap; word-break: normal; }
.md-typeset table:not([class]) td:nth-child(2) { min-width: 6rem; }
.md-typeset table:not([class]) td:nth-child(3) { min-width: 15rem; }
.md-typeset table:not([class]) td:nth-child(4) { min-width: 7rem; }
/* Wider pages than Material's 61rem: the reference tables have four columns. */
.md-grid { max-width: 90rem; }
.md-typeset table:not([class]) td code { word-break: break-word; }
.md-typeset table:not([class]) th { background: var(--hd-bg-raised); font-family: "IM Fell English SC", Georgia, serif; font-weight: 400; }
/* Home page: the moon and the mist (overrides/home.html). */
.hd-hero {
position: relative;
overflow: hidden;
text-align: center;
padding: 7rem 1rem 6.5rem;
background:
radial-gradient(circle at 78% 22%, #eef2f9 0 42px, rgba(220, 228, 244, .35) 43px, rgba(200, 212, 236, .10) 110px, transparent 210px),
radial-gradient(ellipse 70% 40% at 50% 100%, rgba(160, 175, 205, .22), transparent 70%),
linear-gradient(#1c2335, var(--hd-bg) 75%);
}
.hd-hero::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 160px;
background: linear-gradient(transparent, rgba(190, 200, 220, .10) 60%, var(--hd-bg));
}
.hd-hero__content { position: relative; z-index: 1; }
.hd-hero h1 { margin: 0; font: 400 clamp(2.4rem, 6vw, 4.4rem) "IM Fell English", Georgia, serif; color: var(--hd-bright); }
.hd-hero p { margin: .9rem 0 0; color: #b8c2d3; font-style: italic; font-size: 1.3rem; }
.hd-buttons { display: flex; gap: .9rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem; }
.hd-button {
padding: .75rem 1.75rem;
font: 1.1rem "IM Fell English SC", Georgia, serif;
color: #e9edf3 !important;
border: 1px solid rgba(220, 228, 240, .45);
border-radius: 2px;
background: rgba(15, 18, 24, .55);
transition: background .2s, border-color .2s;
}
.hd-button:hover { background: rgba(255, 255, 255, .1); border-color: rgba(240, 244, 250, .8); }
.hd-button--primary { background: rgba(228, 234, 245, .92); color: #141822 !important; border-color: transparent; }
.hd-button--primary:hover { background: #fff; }
.hd-home { max-width: 54rem; margin: 0 auto; }
.hd-cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin: 1.2rem 0 2rem; }
.hd-card {
display: block;
border: 1px solid var(--hd-line);
background: rgba(160, 180, 215, .05);
padding: 1.1rem 1.2rem;
border-radius: 4px;
color: var(--hd-text) !important;
transition: border-color .2s, background .2s;
}
.hd-card:hover { border-color: #5b6a86; background: rgba(160, 180, 215, .09); }
.hd-card strong { display: block; font: 1.25rem "IM Fell English SC", Georgia, serif; font-weight: 400; color: #dfe5ef; margin-bottom: .3rem; }
.hd-card span { color: var(--hd-muted); font-size: .85rem; }
@media screen and (max-width: 44.9375em) {
/* A smaller moon in the corner: in the middle it covered the title. */
.hd-hero {
padding: 4.5rem 1rem 4rem;
background:
radial-gradient(circle at 86% 11%, #eef2f9 0 22px, rgba(220, 228, 244, .35) 23px, rgba(200, 212, 236, .10) 60px, transparent 120px),
radial-gradient(ellipse 70% 40% at 50% 100%, rgba(160, 175, 205, .22), transparent 70%),
linear-gradient(#1c2335, var(--hd-bg) 75%);
}
.hd-cards { grid-template-columns: 1fr; }
}
+133 -131
View File
@@ -140,162 +140,164 @@ The previous frame's matrices are not there; the port keeps them itself.
- Missing: motion of animated objects (characters, cloth, foliage), reactive/transparency
masks for particles and fog, render-resolution scaling, frame generation.
## 2026-10-01: FSR 4 при выводе 1440p/2160p не работал (мерцание и дрожание)
## 2026-10-01: FSR 4 did not work at 1440p/2160p output (flicker and shimmer)
Видео `video_2026-10-01_03-58-59.mp4` (вывод 3840x2160, FSR 4 Performance): мерцание и дрожание
всех объектов. Причина — апскейлер в этом режиме вообще не выполнялся, а jitter оставался
включённым: на экран шёл растянутый кадр сцены, каждый кадр сдвинутый на свою фазу Halton.
Video `video_2026-10-01_03-58-59.mp4` (3840x2160 output, FSR 4 Performance): flicker and shimmer
on all objects. Cause: the upscaler did not run at all in this mode, while jitter stayed
enabled. The screen showed a stretched scene frame, each frame shifted by its own Halton phase.
1. Проход UI опознавался по точному размеру цели `BB_RENDER_RES` (1916x1078), а игра выделяет
цели с выровненной высотой (1916x1080; в константах сцены тоже 1916x1080). `RunScaled` не
вызывался ни разу (в логе не было `UI: native composition`). Теперь допускается выравнивание
до 8 пикселей (`RenderTarget`), а размер сцены для FSR берётся из констант сцены
(`CameraMotion::RenderSize`, `SceneSize`).
2. После этого FSR 4 падал с `external image registration failed (-1000069000)`: `RunScaled`
создавал новые image view каждый кадр, а реестр FSR 4 вмещает восемь. Теперь те же
`CachedView`, что и в пути Native AA.
1. The UI pass was identified by the exact `BB_RENDER_RES` target size (1916x1078), but the game
allocates targets with an aligned height (1916x1080; the scene constants also say 1916x1080).
`RunScaled` was never called (the log had no `UI: native composition`). Alignment up to
8 pixels is now accepted (`RenderTarget`), and the scene size for FSR comes from the scene
constants (`CameraMotion::RenderSize`, `SceneSize`).
2. After that FSR 4 failed with `external image registration failed (-1000069000)`: `RunScaled`
created new image views every frame, and the FSR 4 registry holds eight. It now uses the same
`CachedView` as the Native AA path.
Проверка дампом (`BB_DUMP_TRIGGER=<файл> BB_DUMP_DIR=<каталог>`, `BB_DUMP_FRAMES`, по умолчанию 8:
вход FSR, векторы движения и выход, raw): при неподвижной камере PSNR соседних кадров выхода
~45 дБ против ~28 дБ у входа с jitter; при повороте камеры и ходьбе шлейфов нет. FPS в этом
режиме ~107 вместо ~220 — раньше FSR 4 просто не выполнялся.
Dump check (`BB_DUMP_TRIGGER=<file> BB_DUMP_DIR=<dir>`, `BB_DUMP_FRAMES`, default 8:
FSR input, motion vectors and output, raw): with a static camera the PSNR of adjacent output frames
is ~45 dB against ~28 dB for the jittered input; no trails when turning the camera or walking. FPS in
this mode is ~107 instead of ~220; before, FSR 4 simply did not run.
Осталось: спрайты (по 4 индекса) в цвет сцены со сценической глубиной — свечения, огоньки —
по-прежнему не сдвигаются jitter (правило «≤ 6 индексов = полноэкранный проход»).
Remaining: sprites (4 indices each) drawn into scene color with scene depth (glows, small lights)
are still not shifted by jitter (the rule "≤ 6 indices = full-screen pass").
## 2026-10-01: FSR 4 быстрее — проход post (2.9 → 0.8 мс в 4K)
## 2026-10-01: Faster FSR 4 — the post pass (2.9 → 0.8 ms at 4K)
Замеры: `BB_FSR4_PROFILE=1` (время каждого прохода FSR 4, патч провайдера в субмодуле,
см. `gpu/patches/fsr-vulkan`), бенчмарк вне игры `out/gpu/fsr4-bench` (`ninja -C out/gpu
fsr4-bench`; `--stats` — регистры и инструкции от RADV). 4K Balanced (2260x1272 → 3840x2160),
RX 7800 XT: весь FSR 4 — 5.8 мс, из них **post 2.5–2.9 мс** (не нейросеть: последние слои,
pixel shuffle 2x2 и смешивание с историей), 12 проходов модели — 2.4 мс, pre 0.55.
Measurements: `BB_FSR4_PROFILE=1` (time of each FSR 4 pass, a provider patch in the submodule,
see `gpu/patches/fsr-vulkan`), the out-of-game benchmark `out/gpu/fsr4-bench` (`ninja -C out/gpu
fsr4-bench`; `--stats` prints registers and instructions from RADV). 4K Balanced (2260x1272 → 3840x2160),
RX 7800 XT: all of FSR 4 takes 5.8 ms, of which **post is 2.5–2.9 ms** (not the neural network: the last layers,
2x2 pixel shuffle and blending with history), the 12 model passes 2.4 ms, pre 0.55.
Причина: каждый поток считает блок 2x2 выходных пикселей и пишет их по одному в три образа
(рекуррентное состояние, история, выход) — каждая инструкция записи волны пишет пиксели через
один. Убрав любую из трёх записей, проход ускорялся в 1.3–4 раза при том же коде.
Cause: each thread computes a 2x2 block of output pixels and writes them one by one into three images
(recurrent state, history, output), so each wave store instruction writes every other
pixel. Removing any one of the three stores made the pass 1.3–4 times faster with the same code.
Решение: `tools/fsr4_optimize.sh` декомпилирует post (spirv-cross), `tools/fsr4_post_lds.pl`
собирает блок 16x16 рабочей группы в shared memory и пишет сплошными строками, glslang
компилирует обратно в `fsr4_shaders/opt/`; `vk_fsr4.cpp` берёт его оттуда (`BB_FSR4_OPT=0` —
оригинал). Две тонкости, найденные сравнением выходов:
- spirv-cross переводит знаковую распаковку int8 (`OpBitcast` в `i8vec4`) как `unpack8(uint)`
(беззнаковую) — без исправления результат совсем другой (PSNR 17 дБ);
- значения в shared memory должны оставаться float: half из shared memory компилятор
превращает в 16-битную запись, а она иначе округляет в unorm8.
Fix: `tools/fsr4_optimize.sh` decompiles post (spirv-cross), `tools/fsr4_post_lds.pl`
gathers the workgroup's 16x16 block in shared memory and writes contiguous rows, and glslang
compiles it back into `fsr4_shaders/opt/`; `vk_fsr4.cpp` loads it from there (`BB_FSR4_OPT=0` selects the
original). Two subtleties, found by comparing outputs:
- spirv-cross translates the signed int8 unpack (`OpBitcast` to `i8vec4`) as `unpack8(uint)`
(unsigned); without a fix the result is completely different (PSNR 17 dB);
- values in shared memory must stay float: the compiler turns a half from shared memory
into a 16-bit store, which rounds differently to unorm8.
`tools/fsr4_verify.sh` сравнивает оригинал и оптимизированный post для всех пресетов при выводе
1080p/1440p/4K на псевдослучайных входах: 1440p и 4K — побитово одинаково; post 4K 2.1–3.5 →
0.81–0.87 мс, 1440p 1.2–1.5 → 0.36–0.38 мс, 1080p 0.37–0.69 → 0.20–0.22 мс. В игре (4K
Balanced): FSR 4 5.8 → 4.0 мс, кадр GPU 13.1 → ~11.6 мс; дальше FPS упирается в CPU (поток
GPU-команд ждёт копии гостевой памяти, «host copies» ~20%).
`tools/fsr4_verify.sh` compares the original and the optimized post for all presets at
1080p/1440p/4K output on pseudo-random inputs: 1440p and 4K are bit-identical; post at 4K 2.1–3.5 →
0.81–0.87 ms, 1440p 1.2–1.5 → 0.36–0.38 ms, 1080p 0.37–0.69 → 0.20–0.22 ms. In game (4K
Balanced): FSR 4 5.8 → 4.0 ms, GPU frame 13.1 → ~11.6 ms; beyond that FPS is CPU-bound (the
GPU command thread waits for guest memory copies, "host copies" ~20%).
Найдено попутно: **оригинальный FSR 4 на уровне 1080 (вывод 1920x1080) недетерминирован** — от
запуска к запуску на одинаковых входах меняется полоса у левого края (столбцы 0–132), то есть
где-то гонка или чтение неинициализированной памяти в модели/провайдере. При выводе 1080p это
может давать мерцание у левого края кадра. Не исследовано.
Found along the way: **the original FSR 4 at the 1080 tier (1920x1080 output) is nondeterministic**: from
run to run on identical inputs a strip at the left edge changes (columns 0–132), so there is
a race or a read of uninitialized memory somewhere in the model/provider. At 1080p output this
can cause flicker at the left edge of the frame. Not investigated.
Маска реактивности при FSR 4 больше не считается (FSR 4 её не принимает).
The reactive mask is no longer computed with FSR 4 (FSR 4 does not accept it).
### Что ещё проверено (там же, 4K Balanced)
### What else was checked (same setup, 4K Balanced)
- **A/B в одном процессе** (`ab.sh`, бит 24 — FSR выключен, UI-копия без апскейла): 86.5 FPS с
FSR 4 против 87.7 без него. После оптимизации post кадр упирается в CPU (поток GPU-команд,
ожидание копий гостевой памяти), а не в GPU. Async compute для FSR (перекрыть его с началом
следующего кадра) дал бы не больше этого ~1% — отложено до ускорения CPU-части; к тому же он
требует переставлять команды UI и вывода кадра N после работы кадра N+1.
- **WMMA (`VK_KHR_cooperative_matrix`, RADV на RDNA3 поддерживает)**: прототип прохода 1
модели (остаточный блок 16 каналов: 3x3 16→16, 1x1 16→32 ReLU, 1x1 32→16) — 0.85 мс в первом
варианте, 0.49 мс со словной раскладкой в shared memory, против 0.30 мс у исходного dot4.
Арифметика на WMMA заняла бы ~0.1 мс, но при 16 каналах выкладка тайлов, эпилоги через shared
memory и занятость (6 волн/SIMD, предел по LDS) съедают выигрыш. Потолок — около 1 мс на
все 12 проходов при тонкой ручной переработке каждого; не начато.
- `RADV_PERFTEST=cswave32` (wave32 для compute): FSR 4 медленнее, 4.2 → 5.4 мс.
- Проход pre (0.54 мс) запись не ограничивает (0.50 мс без записи).
- **A/B in one process** (`ab.sh`, bit 24 turns FSR off, UI copy without upscaling): 86.5 FPS with
FSR 4 against 87.7 without it. After the post optimization the frame is CPU-bound (GPU command thread,
waiting for guest memory copies), not GPU-bound. Async compute for FSR (overlapping it with the start
of the next frame) would gain no more than this ~1%; postponed until the CPU side is faster. It also
requires reordering the UI and present commands of frame N after the work of frame N+1.
- **WMMA (`VK_KHR_cooperative_matrix`, supported by RADV on RDNA3)**: a prototype of model pass 1
(16-channel residual block: 3x3 16→16, 1x1 16→32 ReLU, 1x1 32→16): 0.85 ms in the first
version, 0.49 ms with a word layout in shared memory, against 0.30 ms for the original dot4.
The arithmetic on WMMA would take ~0.1 ms, but with 16 channels the tile layout, epilogues through shared
memory and occupancy (6 waves/SIMD, limited by LDS) eat the gain. The ceiling is about 1 ms for
all 12 passes with careful manual rework of each; not started.
- `RADV_PERFTEST=cswave32` (wave32 for compute): FSR 4 is slower, 4.2 → 5.4 ms.
- The pre pass (0.54 ms) is not limited by stores (0.50 ms without stores).
## 2026-10-01: мерцание FSR 4 у левого края при выводе 1080p — гонка в проходе 11 модели
## 2026-10-01: FSR 4 flicker at the left edge at 1080p output — a race in model pass 11
Недетерминизм уровня 1080 (см. выше) — ошибка в шейдере модели v07, проход 11 (декодер,
1/4 → 1/2 разрешения). Каждый поток — пиксель входа 1/4 разрешения и пишет блок 2x2 выхода
1/2 разрешения. Диспатч округляет ширину до 64 потоков, а потоки за шириной входа не
останавливаются: на уровне 1080 (вход 480 в ширину) потоки 480..511 пишут пиксели выхода
960..1023, то есть первые пиксели следующей строки (строка любого тензора — 15360 байт), и
гоняются с их настоящими авторами. При выводе 4K ширина 960 делится на 64, ошибки нет.
The 1080-tier nondeterminism (see above) is a bug in the v07 model shader, pass 11 (decoder,
1/4 → 1/2 resolution). Each thread is one pixel of the 1/4-resolution input and writes a 2x2 block of the
1/2-resolution output. The dispatch rounds the width up to 64 threads, and threads beyond the input width do not
exit: at the 1080 tier (input 480 wide) threads 480..511 write output pixels
960..1023, which are the first pixels of the next row (a row of any tensor is 15360 bytes), and
race with their real writers. At 4K output the width 960 is divisible by 64, so there is no bug.
`tools/fsr4_pass11_guard.pl` добавляет ранний выход для потоков за границей (размер берётся из
проверки соседей самого прохода); `tools/fsr4_optimize.sh` собирает его в `fsr4_shaders/opt`
для всех пресетов вместе с post; `vk_fsr4.cpp` и бенчмарк берут из `opt/` любой проход.
`tools/fsr4_verify.sh`: 1440p/4K — побитово как оригинал (защита там не срабатывает); 1080p —
одинаковый результат от запуска к запуску, отличия от оригинала после первого кадра только в
бывшей полосе у левого края.
`tools/fsr4_pass11_guard.pl` adds an early exit for out-of-bounds threads (the size comes from
the pass's own neighbor check); `tools/fsr4_optimize.sh` builds it into `fsr4_shaders/opt`
for all presets together with post; `vk_fsr4.cpp` and the benchmark load any pass from `opt/`.
`tools/fsr4_verify.sh`: 1440p/4K are bit-identical to the original (the guard does not trigger there); 1080p gives
the same result from run to run, and after the first frame it differs from the original only in
the former strip at the left edge.
Найдено при этом: spirv-cross переводит все знаковые распаковки int8 (`OpBitcast` в `v4char`)
как беззнаковый `unpack8(uint)`; в pass 11 их девять разных форм. Исправление общее для
скриптов — `tools/Fsr4SpirvCrossFixes.pm` (все `unpack8` через знаковый помощник).
Found in the process: spirv-cross translates all signed int8 unpacks (`OpBitcast` to `v4char`)
as unsigned `unpack8(uint)`; pass 11 has nine different forms of them. The fix is shared by the
scripts: `tools/Fsr4SpirvCrossFixes.pm` (all `unpack8` go through a signed helper).
## 2026-10-01: FSR 4.1.1 — что внутри и как его запустить в Vulkan (разведка)
## 2026-10-01: FSR 4.1.1 — what is inside and how to run it in Vulkan (investigation)
Источник: `amd_fidelityfx_upscaler_dx12.dll` 4.1.1.2740 из OptiScaler (`FSR4_LATEST`).
- В DLL есть провайдер `ffxProvider_FSR4_Int8` / `Fsr4Int8UpscalerModel` (версия «FSR4-i8 4.1.1»).
1028 DXIL-контейнеров (25.7 МБ): проходы модели `fsr4_model_v07_fp8_no_scale_pass1..12` (+ `_post`),
270 вариантов `prepass`, 144 `postpass`, `fsr_rcas_pass` и запасной FSR 3.1/2. Сжатых данных нет.
- Несмотря на «fp8» в имени, проходы модели — обычный DXIL с `dot4AddPacked` (int8 dot4), без
расширений AMD (WMMA). Архитектура та же v07 (12 проходов), но веса и смещения не зашиты в
шейдер, а читаются из `InitializerBuffer` (t18, typed buffer), размеры тензоров — из cbuffer
`CsTensorSizes`, scratch — `u11`. У каждого прохода 9 вариантов: сетка зашита (уровни 1080 / 4K /
8K: 480x270, 960x540, 1920x1080 для прохода 5) и, видимо, пресеты.
- `amdxcffx64.dll` (загрузчик 2.3.0.2913, его подгружает Proton) — те же FP8-проходы FSR 4 и ML-
генерация кадров (`mlfi_*`), отдельной INT8-модели нет. `FSR4_INT8` 4.0.2 — это `v07_i8`, то есть
то, что порт уже использует.
- dxil-spirv (HansKristian-Work/dxil-spirv, собирается из исходников) переводит проходы в
корректный SPIR-V: `dxil-spirv <pass> --enable-shader-i8-dot --ssbo-uav --use-reflection-names`.
Source: `amd_fidelityfx_upscaler_dx12.dll` 4.1.1.2740 from OptiScaler (`FSR4_LATEST`).
- The DLL contains the provider `ffxProvider_FSR4_Int8` / `Fsr4Int8UpscalerModel` (version "FSR4-i8 4.1.1").
1028 DXIL containers (25.7 MB): model passes `fsr4_model_v07_fp8_no_scale_pass1..12` (+ `_post`),
270 `prepass` variants, 144 `postpass`, `fsr_rcas_pass` and the FSR 3.1/2 fallback. There is no compressed data.
- Despite "fp8" in the name, the model passes are plain DXIL with `dot4AddPacked` (int8 dot4), without
AMD extensions (WMMA). The architecture is the same v07 (12 passes), but weights and biases are not baked into
the shader; they are read from `InitializerBuffer` (t18, typed buffer), tensor sizes from the cbuffer
`CsTensorSizes`, scratch is `u11`. Each pass has 9 variants: the grid is baked in (1080 / 4K /
8K tiers: 480x270, 960x540, 1920x1080 for pass 5) and, apparently, the presets.
- `amdxcffx64.dll` (loader 2.3.0.2913, loaded by Proton) has the same FP8 FSR 4 passes and ML
frame generation (`mlfi_*`), with no separate INT8 model. `FSR4_INT8` 4.0.2 is `v07_i8`, that is,
what the port already uses.
- dxil-spirv (HansKristian-Work/dxil-spirv, built from source) translates the passes into
correct SPIR-V: `dxil-spirv <pass> --enable-shader-i8-dot --ssbo-uav --use-reflection-names`.
Чего не хватает для своего провайдера 4.1.1: соответствие контейнеров (пресет, уровень, проход),
содержимое `InitializerBuffer`, значения `CsTensorSizes`, перестановки prepass/postpass, константы и
размеры диспатчей. Самый надёжный путь — не разбирать x86-код провайдера, а записать один кадр:
маленькая тестовая программа на FFX API (DX12, MinGW) под Wine/Proton с vkd3d-proton, явный выбор
провайдера «FSR4-i8» через override версии, и Vulkan-слой, который пишет пайплайны (SPIR-V от
dxil-spirv), содержимое буферов и размеры диспатчей. Затем — воспроизведение своим провайдером
через тот же бенчмарк (`fsr4-bench`), с проверкой против записанного выхода.
What is missing for an own 4.1.1 provider: the container mapping (preset, tier, pass),
the contents of `InitializerBuffer`, the `CsTensorSizes` values, the prepass/postpass permutations, constants and
dispatch sizes. The most reliable way is not to reverse the provider's x86 code but to record one frame:
a small FFX API (DX12, MinGW) test program under Wine/Proton with vkd3d-proton, explicit selection of
the "FSR4-i8" provider through a version override, and a Vulkan layer that writes the pipelines (SPIR-V from
dxil-spirv), buffer contents and dispatch sizes. Then replay with an own provider
through the same benchmark (`fsr4-bench`), checked against the recorded output.
## 2026-10-01: FSR 4.1.1 в порту (upscaler=fsr411)
## 2026-10-01: FSR 4.1.1 in the port (upscaler=fsr411)
Модель AMD 4.1.1 (INT8) работает нативно в Vulkan и совпадает с DLL до бита.
The AMD 4.1.1 (INT8) model runs natively in Vulkan and matches the DLL bit for bit.
**Как получено.** `tools/fsr4cap/fsr4cap.exe` (C, MinGW) вызывает FidelityFX API 2.3 из
`amd_fidelityfx_loader_dx12.dll` с явной версией 4.1.1 и через подмену таблиц методов D3D12
записывает всё, что делает DLL под vkd3d-proton: пайплайны (DXIL), корневые сигнатуры,
дескрипторы, загрузки, константы, диспатчи. Кадр — 29 диспатчей: SPD авто-экспозиции, prepass,
pass0_post, проходы модели 1..12 (за каждым `_post`, обнуление рамки тензора), postpass, RCAS.
- Моделей две: m0 (Native..Performance) и m1 (Ultra Performance, соотношение 3.0), веса — 128 КБ
(`InitializerBuffer`). Уровни: t1080 (вывод до 1920x1080), t2160 (больше, вкл. 2560x1080).
- Тензоры считаются от вывода, выровненного до 8; таблица `CsTensorSizes` (17 размеров), число
групп и константы (MLSR как у v07, но width/height не выровнены и `inv_scale = 1 / scale`;
SPD; RCAS) проверяются `extract.py` на 20 записях.
- dxil-spirv переводит DXIL так же, как vkd3d-proton: `--mixed-float-dot-product`
(`VK_VALVE_shader_mixed_float_dot_product`, dot2 по half с накоплением во float) — без него
PSNR 40 дБ вместо точного совпадения. `--class-bindings` (патч) раскладывает регистры SRV/UAV/
CBV/сэмплеров по биндингам +0/+32/+64/+96, рантайм привязывает ресурсы по именам.
- `fsr411/fsr411.cpp` — независимое ядро на Vulkan (игра и `fsr4-bench --fsr411`).
- `verify.sh`: DLL и воспроизведение на одинаковых входах, 8 кадров — побитово одинаково для
1080p (обе модели), 1440p, 2560x1080, 4K (обе модели). 1600x900 расходится только у правого
края, где сама DLL недетерминирована (гонка в её шейдерах при ширине тензора не кратной 64).
**How it was obtained.** `tools/fsr4cap/fsr4cap.exe` (C, MinGW) calls the FidelityFX API 2.3 from
`amd_fidelityfx_loader_dx12.dll` with an explicit version 4.1.1 and, by replacing D3D12 method tables,
records everything the DLL does under vkd3d-proton: pipelines (DXIL), root signatures,
descriptors, uploads, constants, dispatches. (Since then fsr4cap.exe loads AMD's upscaler DLL directly,
which exports the API itself; the loader is only needed for a DLL that does not.) A frame is 29 dispatches: auto-exposure SPD, prepass,
pass0_post, model passes 1..12 (each followed by `_post`, which clears the tensor border), postpass, RCAS.
- There are two models: m0 (Native..Performance) and m1 (Ultra Performance, ratio 3.0); weights are 128 KB
(`InitializerBuffer`). Tiers: t1080 (output up to 1920x1080), t2160 (larger, incl. 2560x1080).
- Tensors are computed from the output aligned to 8; the `CsTensorSizes` table (17 sizes), the group
counts and the constants (MLSR as in v07, but width/height are not aligned and `inv_scale = 1 / scale`;
SPD; RCAS) are checked by `extract.py` on 20 recordings.
- dxil-spirv translates DXIL the same way as vkd3d-proton: `--mixed-float-dot-product`
(`VK_VALVE_shader_mixed_float_dot_product`, half dot2 with float accumulation); without it
PSNR is 40 dB instead of an exact match. `--class-bindings` (a patch) lays out SRV/UAV/
CBV/sampler registers at bindings +0/+32/+64/+96; the runtime binds resources by name.
- `fsr411/fsr411.cpp` is an independent Vulkan core (the game and `fsr4-bench --fsr411`).
- `verify.sh`: DLL and replay on identical inputs, 8 frames, bit-identical for
1080p (both models), 1440p, 2560x1080, 4K (both models). 1600x900 differs only at the right
edge, where the DLL itself is nondeterministic (a race in its shaders when the tensor width is not a multiple of 64).
**Ускорение.** postpass переписан через workgroup memory на уровне SPIR-V-ассемблера
(`postpass_lds.py`; перевод через GLSL неточен: теряются `DenormPreserve` и точная расстановка
`RelaxedPrecision`, и ещё что-то меняет результат): 2.18 → 0.95 мс в 4K, побитово как DLL.
Весь FSR 4.1.1: 1.09 мс при 1080p (1280x720), 4.2 мс при 4K Balanced. В игре (4K Balanced)
апскейлер 4.12 мс против 4.32 мс у ускоренного v07.
**Speedup.** postpass is rewritten to use workgroup memory at the SPIR-V assembly level
(`postpass_lds.py`; going through GLSL is inexact: `DenormPreserve` and the exact placement of
`RelaxedPrecision` are lost, and something else changes the result too): 2.18 → 0.95 ms at 4K, bit-identical to the DLL.
All of FSR 4.1.1: 1.09 ms at 1080p (1280x720), 4.2 ms at 4K Balanced. In game (4K Balanced)
the upscaler takes 4.12 ms against 4.32 ms for the accelerated v07.
**Сборка ассетов** (пользователем, из своих DLL; в репозитории ничего от AMD нет):
**Building the assets** (by the user, from their own DLLs; the repository contains nothing from AMD):
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll 4.1.x> <amd_fidelityfx_loader_dx12.dll 2.3.x>
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll 4.1.x> [<amd_fidelityfx_loader_dx12.dll 2.3.x>]
Нужны nix-shell (или MinGW, cmake, ninja, python3, SPIRV-Tools, umu-run), сеть для dxil-spirv и
заголовков FidelityFX SDK и Proton (GE-Proton). DLL есть, например, у OptiScaler
(`FSR4_LATEST/`) и в играх с FSR 3.1/4. Сборка с нуля даёт те же файлы; `VERIFY=1` в конце
сравнивает с DLL.
Needs nix-shell (or MinGW, cmake, ninja, python3, SPIRV-Tools, git), network access for dxil-spirv, and
Proton (PROTONPATH, or GE-Proton 10+ / Proton-CachyOS / Proton Experimental; umu-run or Steam's runtime).
In the AppImage the tools are prebuilt (`BB_FSR4CAP_TOOLS`). The DLLs are available, for example, from OptiScaler
(`FSR4_LATEST/`) and in games with FSR 3.1/4. A build from scratch produces the same files; `VERIFY=1` at the end
compares against the DLL.
**Поправка к A/B выше («кадр упирается в CPU»).** Те замеры шли, пока 12 зависших процессов
shadPS4 занимали по ядру. На свободной системе: «host copies» 1.6%, ожидание GPU ~41% — кадр
упирается в GPU, так что async compute для FSR снова имеет смысл.
**Correction to the A/B above ("the frame is CPU-bound").** Those measurements ran while 12 hung shadPS4
processes each occupied a core. On an idle system: "host copies" 1.6%, waiting for the GPU ~41%; the frame
is GPU-bound, so async compute for FSR makes sense again.
+301 -102
View File
@@ -27,9 +27,22 @@ if (BB_LTO)
message(WARNING "LTO unavailable: ${bb_ipo_error}")
endif()
endif()
if (BB_PGO STREQUAL "generate")
if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND BB_PGO STREQUAL "generate")
# Clang (the Windows build, llvm-mingw): instrumented, the profile written to BB_PGO_FILE's
# directory as bbport-%p.profraw (merged with llvm-profdata into BB_PGO_FILE for "use").
add_compile_options(-fprofile-instr-generate -fprofile-update=atomic)
add_link_options(-fprofile-instr-generate)
add_compile_definitions(BB_PGO_GENERATE BB_PGO_CLANG)
elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND BB_PGO STREQUAL "use")
set(BB_PGO_FILE "${BB_PGO_DIR}/bbport.profdata" CACHE FILEPATH "Merged Clang profile")
add_compile_options(-fprofile-instr-use=${BB_PGO_FILE} -Wno-profile-instr-out-of-date
-Wno-profile-instr-unprofiled -Wno-backend-plugin)
elseif (BB_PGO STREQUAL "generate")
# Atomic counters: the GPU, recording, preparation and copy threads run the same code.
add_compile_options(-fprofile-generate=${BB_PGO_DIR} -fprofile-update=atomic)
# Profile files named relative to the build directory (not its absolute path): moving the
# checkout (game_files/native_probe -> bbport) left a profile nothing matched, silently.
add_compile_options(-fprofile-generate=${BB_PGO_DIR} -fprofile-update=atomic
-fprofile-prefix-path=${CMAKE_BINARY_DIR})
add_link_options(-fprofile-generate=${BB_PGO_DIR})
add_compile_definitions(BB_PGO_GENERATE)
elseif (BB_PGO STREQUAL "use")
@@ -37,24 +50,57 @@ elseif (BB_PGO STREQUAL "use")
# threads can be slightly inconsistent (correction). Functions changed since the profile
# (or built differently for it, bbgpu_init) are compiled without it.
add_compile_options(-fprofile-use=${BB_PGO_DIR} -fprofile-partial-training
-fprofile-prefix-path=${CMAKE_BINARY_DIR}
-fprofile-correction -Wno-missing-profile -Wno-coverage-mismatch)
endif()
find_package(Vulkan REQUIRED)
find_package(fmt REQUIRED)
find_package(Boost REQUIRED)
find_package(magic_enum REQUIRED)
find_package(magic_enum QUIET)
if(NOT magic_enum_FOUND)
# Header-only; packaged on few systems (the Windows cross build's MSYS2 has none).
include(FetchContent)
FetchContent_Declare(magic_enum GIT_REPOSITORY https://github.com/Neargye/magic_enum.git GIT_TAG v0.9.7)
FetchContent_MakeAvailable(magic_enum)
endif()
find_package(tsl-robin-map REQUIRED)
find_package(VulkanMemoryAllocator CONFIG REQUIRED)
find_package(VulkanMemoryAllocator CONFIG QUIET)
if (NOT TARGET GPUOpen::VulkanMemoryAllocator)
# The header alone, without its CMake package (MSYS2: include/vma/vk_mem_alloc.h).
find_path(VMA_INCLUDE_DIR vk_mem_alloc.h PATH_SUFFIXES vma REQUIRED)
add_library(GPUOpen::VulkanMemoryAllocator INTERFACE IMPORTED)
target_include_directories(GPUOpen::VulkanMemoryAllocator INTERFACE ${VMA_INCLUDE_DIR})
endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3)
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
find_package(miniz REQUIRED)
if (NOT WIN32)
pkg_check_modules(X11 REQUIRED IMPORTED_TARGET x11)
endif()
find_package(miniz QUIET)
if(NOT miniz_FOUND)
# Distributions without a miniz package (Debian, Ubuntu: #136): built from its release.
include(FetchContent)
FetchContent_Declare(miniz GIT_REPOSITORY https://github.com/richgel999/miniz.git GIT_TAG 3.1.2)
FetchContent_MakeAvailable(miniz)
if(NOT TARGET miniz::miniz)
add_library(miniz::miniz ALIAS miniz)
endif()
endif()
find_package(Zydis REQUIRED)
find_program(GLSLANG_COMPILER NAMES glslang glslangValidator REQUIRED)
set(SIRIT_USE_SYSTEM_SPIRV_HEADERS OFF CACHE BOOL "" FORCE)
if (WIN32)
# Every target, the third-party ones too: what glibc and MSVC have and MinGW does not.
include_directories(BEFORE platform/win32/include)
enable_language(RC)
# Bit-fields laid out as on Linux (GCC's way), not MSVC's: the GPU registers and the PS4's
# structures are declared with them.
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-mno-ms-bitfields>)
add_compile_options("$<$<COMPILE_LANGUAGE:C,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/platform/win32/include/bbport_win32.h>")
endif()
add_subdirectory(third_party/sirit EXCLUDE_FROM_ALL)
# bbport: FSR 3.1 temporal upscaling / frame generation, native Vulkan (MIT, FireBurn/FSR-Vulkan).
@@ -62,143 +108,237 @@ set(FFX_VK_PORTABLE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(FFX_VK_PORTABLE_BUILD_PROBE OFF CACHE BOOL "" FORCE)
add_subdirectory(third_party/fsr-vulkan EXCLUDE_FROM_ALL)
# Host shaders -> headers (same scheme as shadPS4's host_shaders/CMakeLists.txt).
set(SHADER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/shadps4/video_core/host_shaders)
# Host shaders -> headers: SPIR-V arrays from glslang, in bbgpu/shaders/.
set(SHADER_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/include)
set(SHADER_DIR ${SHADER_INCLUDE}/video_core/host_shaders)
set(SHADER_HEADERS "")
function(add_host_shader FILENAME SHADER_NAME)
set(OUT ${SHADER_DIR}/${SHADER_NAME}.h)
function(add_shader_header SOURCE OUT_DIR SHADER_NAME)
set(OUT ${OUT_DIR}/${SHADER_NAME}.h)
string(TOUPPER ${SHADER_NAME} SYMBOL)
set(DEFINES ${ARGN})
list(TRANSFORM DEFINES PREPEND "-D")
add_custom_command(OUTPUT ${OUT}
COMMAND ${CMAKE_COMMAND} -E make_directory ${SHADER_DIR}
COMMAND ${GLSLANG_COMPILER} --target-env vulkan1.3 -o ${OUT} --vn ${SYMBOL} ${DEFINES} ${SHADER_SRC}/${FILENAME}
COMMAND ${CMAKE_COMMAND} "-DHEADER_FILE=${OUT}" -P ${SHADER_SRC}/AddShaderHeaderInclude.cmake
DEPENDS ${SHADER_SRC}/${FILENAME} VERBATIM)
COMMAND ${CMAKE_COMMAND} -E make_directory ${OUT_DIR}
COMMAND ${GLSLANG_COMPILER} --target-env vulkan1.3 -o ${OUT} --vn ${SYMBOL} ${DEFINES}
-I${CMAKE_CURRENT_SOURCE_DIR}/third_party/amd-fsr1 ${SOURCE}
COMMAND ${CMAKE_COMMAND} "-DHEADER_FILE=${OUT}" -P ${CMAKE_CURRENT_SOURCE_DIR}/render/shaders/header.cmake
DEPENDS ${SOURCE} VERBATIM)
set(SHADER_HEADERS ${SHADER_HEADERS} ${OUT} PARENT_SCOPE)
endfunction()
add_host_shader(color_to_ms_depth.frag color_to_ms_depth_frag)
add_host_shader(ms_image_blit.frag ms_image_blit_frag)
add_host_shader(ms_image_blit.frag ms_image_blit_msaa_frag SRC_MSAA=1)
add_host_shader(fault_buffer_process.comp fault_buffer_process_comp)
add_host_shader(fs_tri.vert fs_tri_vert)
add_host_shader(fsr.comp fsr_easu_comp SAMPLE_EASU=1)
add_host_shader(fsr.comp fsr_rcas_comp SAMPLE_RCAS=1)
add_host_shader(post_process.frag post_process_frag)
add_host_shader(camera_motion.comp camera_motion_comp)
add_host_shader(upscale_merge.comp upscale_merge_comp)
add_host_shader(upscale_reactive.comp upscale_reactive_comp)
add_host_shader(taa.comp taa_comp)
add_host_shader(taa_sharpen.comp taa_sharpen_comp)
add_host_shader(taa_sharpen.comp taa_sharpen_ldr_comp LDR_OUTPUT=1)
add_host_shader(fsr4_color.comp fsr4_decode_comp)
add_host_shader(fsr4_color.comp fsr4_encode_comp ENCODE=1)
add_host_shader(fsr4_reactive.comp fsr4_reactive_comp)
add_host_shader(buffer_multi_copy.comp buffer_multi_copy_comp)
add_host_shader(depth_resample.frag depth_resample_frag)
add_host_shader(depth_resample.frag depth_stencil_resample_frag HAS_STENCIL=1)
add_host_shader(depth_resample.frag depth_stencil_bits_frag STENCIL_BITS=1)
add_host_shader(camera_motion.comp camera_motion_debug_comp DEBUG_OVERLAY=1)
set(OWN_SHADER_DIR ${SHADER_INCLUDE}/bbgpu/shaders)
function(add_own_shader FILENAME SHADER_NAME)
add_shader_header(${CMAKE_CURRENT_SOURCE_DIR}/${FILENAME} ${OWN_SHADER_DIR} ${SHADER_NAME} ${ARGN})
set(SHADER_HEADERS ${SHADER_HEADERS} PARENT_SCOPE)
endfunction()
add_own_shader(render/shaders/depth_from_color.frag depth_from_color_frag)
add_own_shader(render/shaders/texel_copy.frag texel_copy_frag)
add_own_shader(render/shaders/texel_copy.frag texel_copy_msaa_frag MSAA_SOURCE=1)
add_own_shader(buffers/shaders/fault_scan.comp fault_scan_comp)
add_own_shader(render/shaders/fullscreen.vert fullscreen_vert)
add_own_shader(render/shaders/present_encode.frag present_encode_frag)
add_own_shader(upscale/shaders/camera_motion.comp camera_motion_comp)
# GT-VBAO in place of the game's SSAO (upscale/vk_vbao.cpp, docs/vbao.md; contributed by redditor69).
add_own_shader(upscale/shaders/vbao.comp vbao_comp)
add_own_shader(upscale/shaders/vbao_blur.comp vbao_blur_comp)
add_own_shader(upscale/shaders/vbao_blur.comp vbao_blur_rgba_comp OUTPUT_RGBA=1)
add_own_shader(upscale/shaders/vbao_view.comp vbao_view_comp)
add_own_shader(upscale/shaders/upscale_merge.comp upscale_merge_comp)
add_own_shader(upscale/shaders/upscale_reactive.comp upscale_reactive_comp)
add_own_shader(upscale/shaders/taa.comp taa_comp)
add_own_shader(upscale/shaders/taa_sharpen.comp taa_sharpen_comp)
add_own_shader(upscale/shaders/fsr1_easu.comp fsr1_easu_comp)
add_own_shader(upscale/shaders/taa_sharpen.comp taa_sharpen_ldr_comp LDR_OUTPUT=1)
add_own_shader(upscale/shaders/fsr4_color.comp fsr4_decode_comp)
add_own_shader(upscale/shaders/fsr4_color.comp fsr4_encode_comp ENCODE=1)
add_own_shader(upscale/shaders/fsr4_reactive.comp fsr4_reactive_comp)
add_own_shader(upscale/shaders/fg_display_lut.comp fg_display_lut_comp)
# SMAA 1x for anti_aliasing=smaa (upscale/vk_smaa.cpp; third_party/smaa, MIT).
add_own_shader(upscale/shaders/smaa.comp smaa_edges_comp PASS=0)
add_own_shader(upscale/shaders/smaa.comp smaa_weights_comp PASS=1)
add_own_shader(upscale/shaders/smaa.comp smaa_blend_comp PASS=2)
# bbport: FSR 3.1 frame generation passes compiled here (ffx_vk.cpp ffxVkShaderOverride): the SDK's
# GLSL with its own headers, as FidelityFX_SC builds them (-Os, Vulkan 1.2), changed where faster.
set(FFX_GPU_DIR ${CMAKE_CURRENT_SOURCE_DIR}/third_party/fsr-vulkan/upstream/ffx-1.1.4/sdk/include/FidelityFX/gpu)
# The port's changed copies of SDK headers: a change to one rebuilds every pass.
file(GLOB_RECURSE FFX_OWN_HEADERS CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/upscale/shaders/ffx/*.h)
function(add_ffx_shader FILENAME SHADER_NAME)
set(OUT ${OWN_SHADER_DIR}/${SHADER_NAME}.h)
string(TOUPPER ${SHADER_NAME} SYMBOL)
set(DEFINES ${ARGN})
list(TRANSFORM DEFINES PREPEND "-D")
add_custom_command(OUTPUT ${OUT}
COMMAND ${CMAKE_COMMAND} -E make_directory ${OWN_SHADER_DIR}
COMMAND ${GLSLANG_COMPILER} --target-env vulkan1.2 -S comp -Os -o ${OUT} --vn ${SYMBOL}
-DFFX_GPU=1 -DFFX_GLSL=1 ${DEFINES} -I${CMAKE_CURRENT_SOURCE_DIR}/upscale/shaders/ffx -I${FFX_GPU_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/${FILENAME}
COMMAND ${CMAKE_COMMAND} "-DHEADER_FILE=${OUT}" -P ${CMAKE_CURRENT_SOURCE_DIR}/render/shaders/header.cmake
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${FILENAME} ${FFX_OWN_HEADERS} VERBATIM)
set(SHADER_HEADERS ${SHADER_HEADERS} ${OUT} PARENT_SCOPE)
endfunction()
add_ffx_shader(upscale/shaders/ffx/of_v5.comp ffx_of_v5 FFX_HALF=0 FFX_OPTICALFLOW_OPTION_HDR_COLOR_INPUT=0)
add_ffx_shader(upscale/shaders/ffx/of_v5.comp ffx_of_v5_hdr FFX_HALF=0 FFX_OPTICALFLOW_OPTION_HDR_COLOR_INPUT=1)
add_ffx_shader(upscale/shaders/ffx/of_v5.comp ffx_of_v5_16 FFX_HALF=1 FFX_OPTICALFLOW_OPTION_HDR_COLOR_INPUT=0)
add_ffx_shader(upscale/shaders/ffx/of_v5.comp ffx_of_v5_16_hdr FFX_HALF=1 FFX_OPTICALFLOW_OPTION_HDR_COLOR_INPUT=1)
# BB_OF_FAST: the finest optical flow level takes the coarser one's vectors (no search).
add_ffx_shader(upscale/shaders/ffx/of_v5.comp ffx_of_v5_fast FFX_HALF=0 FFX_OPTICALFLOW_OPTION_HDR_COLOR_INPUT=0 BB_OF_LEVEL0_PREDICT=1)
add_ffx_shader(upscale/shaders/ffx/of_v5.comp ffx_of_v5_hdr_fast FFX_HALF=0 FFX_OPTICALFLOW_OPTION_HDR_COLOR_INPUT=1 BB_OF_LEVEL0_PREDICT=1)
add_ffx_shader(upscale/shaders/ffx/of_v5.comp ffx_of_v5_16_fast FFX_HALF=1 FFX_OPTICALFLOW_OPTION_HDR_COLOR_INPUT=0 BB_OF_LEVEL0_PREDICT=1)
add_ffx_shader(upscale/shaders/ffx/of_v5.comp ffx_of_v5_16_hdr_fast FFX_HALF=1 FFX_OPTICALFLOW_OPTION_HDR_COLOR_INPUT=1 BB_OF_LEVEL0_PREDICT=1)
add_ffx_shader(upscale/shaders/ffx/fi_interpolation.comp ffx_fi_interpolation FFX_HALF=0)
add_ffx_shader(upscale/shaders/ffx/fi_interpolation.comp ffx_fi_interpolation_check FFX_HALF=0 BB_FI_HARDWARE_BILINEAR=2)
add_own_shader(render/shaders/buffer_multi_copy.comp buffer_multi_copy_comp)
add_own_shader(render/shaders/atlas_decode.comp atlas_decode_comp)
add_own_shader(render/shaders/occlusion_resolve.comp occlusion_resolve_comp)
add_own_shader(render/shaders/gpu_timestamp.comp gpu_timestamp_comp)
add_own_shader(render/shaders/indirect_guard.comp indirect_guard_comp)
add_own_shader(render/shaders/indirect_draw_guard.comp indirect_draw_guard_comp)
add_own_shader(upscale/shaders/depth_resample.frag depth_resample_frag)
add_own_shader(upscale/shaders/depth_resample.frag depth_stencil_resample_frag HAS_STENCIL=1)
add_own_shader(upscale/shaders/depth_resample.frag depth_stencil_bits_frag STENCIL_BITS=1)
add_own_shader(upscale/shaders/camera_motion.comp camera_motion_debug_comp DEBUG_OVERLAY=1)
foreach(BPP IN ITEMS 8 16 32 64 96 128)
add_host_shader(tiling.comp tiling_micro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=0)
add_host_shader(tiling.comp tiling_macro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=1)
add_own_shader(textures/shaders/detile.comp detile_micro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=0)
add_own_shader(textures/shaders/detile.comp detile_macro_${BPP}_comp BITS_PER_PIXEL=${BPP} IS_MACRO_TILED=1)
endforeach()
add_custom_target(host_shaders DEPENDS ${SHADER_HEADERS})
file(GLOB_RECURSE VIDEO_CORE CONFIGURE_DEPENDS shadps4/video_core/*.cpp)
file(GLOB_RECURSE RECOMPILER CONFIGURE_DEPENDS shadps4/shader_recompiler/*.cpp)
list(FILTER VIDEO_CORE EXCLUDE REGEX "renderdoc\\.cpp$")
set(COMMON
shadps4/common/decoder.cpp shadps4/common/error.cpp
shadps4/common/io_file.cpp shadps4/common/native_clock.cpp shadps4/common/number_utils.cpp
shadps4/common/path_util.cpp shadps4/common/rdtsc.cpp shadps4/common/spin_lock.cpp shadps4/common/recursive_lock.cpp
shadps4/common/string_util.cpp shadps4/common/thread.cpp shadps4/common/signal_context.cpp
shim/common/logging/log.cpp)
file(GLOB AVPLAYER CONFIGURE_DEPENDS shadps4/core/libraries/avplayer/*.cpp)
file(GLOB_RECURSE RECOMPILER CONFIGURE_DEPENDS shader/*.cpp)
# Own utilities (types, logging, paths, locks, containers): common/.
file(GLOB COMMON CONFIGURE_DEPENDS common/*.cpp common/logging/*.cpp)
# Own system libraries (Gnm, VideoOut, AvPlayer, event queues, interrupts): platform/.
file(GLOB_RECURSE PLATFORM_SOURCES CONFIGURE_DEPENDS platform/*.cpp)
# platform/win32: the POSIX subset over Win32 (with its headers in platform/win32/include).
if (NOT WIN32)
list(FILTER PLATFORM_SOURCES EXCLUDE REGEX "/platform/win32/")
endif()
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET libavformat libavcodec libavutil libswscale libswresample)
file(GLOB SHIM_SOURCES CONFIGURE_DEPENDS shim/*.cpp shim/*/*.cpp)
add_library(bbgpu SHARED ${VIDEO_CORE} ${RECOMPILER} ${COMMON} ${SHIM_SOURCES}
shadps4/core/libraries/gnmdriver/gnmdriver.cpp
shadps4/core/libraries/videoout/driver.cpp shadps4/core/libraries/videoout/video_out.cpp
shadps4/core/libraries/kernel/equeue.cpp ${AVPLAYER}
shadps4/core/libraries/videodec/video_utils.cpp)
# The layer's own modules (independent of the shadPS4 code): layer/.
file(GLOB LAYER_SOURCES CONFIGURE_DEPENDS layer/*.cpp)
# Game profiles (what the translator knows about one game, out of its core): games/.
file(GLOB GAME_SOURCES CONFIGURE_DEPENDS games/*.cpp games/*/*.cpp)
# The translator's own command processor (docs/TRANSLATOR_ARCHITECTURE.ru.md, step 2): cp/.
file(GLOB CP_SOURCES CONFIGURE_DEPENDS cp/*.cpp memory/*.cpp gcn/*.cpp textures/*.cpp buffers/*.cpp vulkan/*.cpp)
# The translator's rendering pieces (labels, timestamps, occlusion, draw preparation, profiling,
# frame capture, prompts): render/. Temporal upscaling, motion vectors, scene resolution: upscale/.
file(GLOB RENDER_SOURCES CONFIGURE_DEPENDS render/*.cpp)
file(GLOB UPSCALE_SOURCES CONFIGURE_DEPENDS upscale/*.cpp upscale/fsr411/*.cpp)
# Windows: linked into bb-probe.exe (a DLL cannot leave the runtime's symbols to the executable
# that loads it, as -Wl,--allow-shlib-undefined does on Linux).
if (WIN32)
set(BBGPU_LIBRARY_TYPE STATIC)
else()
set(BBGPU_LIBRARY_TYPE SHARED)
endif()
add_library(bbgpu ${BBGPU_LIBRARY_TYPE} ${RECOMPILER} ${COMMON} ${PLATFORM_SOURCES} ${SHIM_SOURCES} ${LAYER_SOURCES} ${GAME_SOURCES} ${CP_SOURCES}
${RENDER_SOURCES} ${UPSCALE_SOURCES})
add_dependencies(bbgpu host_shaders)
# bbport co-op: PS4 network and PSN libraries with the shadNet client, from shadp2p
# (github.com/Wozzardman/shadp2p, GPL-2.0), for private Bloodborne co-op servers.
find_package(Protobuf CONFIG REQUIRED)
set(SHADNET_PROTO_OUT "${CMAKE_CURRENT_BINARY_DIR}/shadnet_proto_gen")
file(MAKE_DIRECTORY "${SHADNET_PROTO_OUT}")
add_custom_command(
OUTPUT "${SHADNET_PROTO_OUT}/shadnet.pb.cc" "${SHADNET_PROTO_OUT}/shadnet.pb.h"
COMMAND protobuf::protoc "--proto_path=${CMAKE_CURRENT_SOURCE_DIR}/shadps4/shadnet"
"--cpp_out=${SHADNET_PROTO_OUT}" "${CMAKE_CURRENT_SOURCE_DIR}/shadps4/shadnet/shadnet.proto"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/shadps4/shadnet/shadnet.proto")
file(GLOB_RECURSE BBNET_SOURCES CONFIGURE_DEPENDS shadps4/core/libraries/network/*.cpp
shadps4/core/libraries/np/*.cpp shadps4/shadnet/*.cpp)
# Trophies and the system dialogs stay in the C runtime; their shadPS4 versions need the
# emulator's UI layers.
list(FILTER BBNET_SOURCES EXCLUDE REGEX
"np_trophy|trophy_ui|np_sns_facebook_dialog|np_profile_dialog/|np_commerce/")
find_package(nlohmann_json CONFIG REQUIRED)
add_library(bbnet STATIC ${BBNET_SOURCES} "${SHADNET_PROTO_OUT}/shadnet.pb.cc" bbnet/bbnet_glue.cpp)
target_include_directories(bbnet PRIVATE shim shadps4 shadps4/shadnet "${SHADNET_PROTO_OUT}"
third_party/imgui third_party/httplib)
target_link_libraries(bbnet PRIVATE nlohmann_json::nlohmann_json)
# HTTPS (the community game server only speaks TLS) and gzip replies in cpp-httplib.
find_package(OpenSSL REQUIRED)
find_package(ZLIB REQUIRED)
target_compile_definitions(bbnet PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT CPPHTTPLIB_ZLIB_SUPPORT)
target_link_libraries(bbnet PRIVATE OpenSSL::SSL OpenSSL::Crypto ZLIB::ZLIB)
# UPnP: opens the co-op port on the router (net_upnp.cpp).
find_path(MINIUPNPC_INCLUDE miniupnpc.h PATH_SUFFIXES miniupnpc REQUIRED)
find_library(MINIUPNPC_LIBRARY NAMES miniupnpc REQUIRED)
target_include_directories(bbnet PRIVATE "${MINIUPNPC_INCLUDE}")
target_link_libraries(bbnet PRIVATE "${MINIUPNPC_LIBRARY}")
target_compile_definitions(bbnet PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
target_compile_options(bbnet PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
target_link_libraries(bbnet PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
protobuf::libprotobuf)
target_link_libraries(bbgpu PUBLIC bbnet)
# In-game settings menu: Dear ImGui (third_party/imgui, MIT) with its Vulkan backend, and an
# embedded Cyrillic font (third_party/fonts).
target_sources(bbgpu PRIVATE third_party/imgui/imgui.cpp third_party/imgui/imgui_draw.cpp
third_party/imgui/imgui_tables.cpp third_party/imgui/imgui_widgets.cpp
third_party/imgui/backends/imgui_impl_vulkan.cpp)
target_include_directories(bbgpu PRIVATE third_party/imgui third_party/imgui/backends)
# Dear ImGui stays private to libbbgpu.so. The library is linked into bb-probe, so its exports are
# in the global symbol scope; a Vulkan layer carrying its own Dear ImGui without -Bsymbolic
# (vkShade 0.1) had its ImGui_ImplVulkan_Init bound to this copy, which called the Vulkan loader
# with the layer's down-chain physical device handle ("Invalid physicalDevice", abort).
set_source_files_properties(third_party/imgui/imgui.cpp third_party/imgui/imgui_draw.cpp
third_party/imgui/imgui_tables.cpp third_party/imgui/imgui_widgets.cpp
third_party/imgui/backends/imgui_impl_vulkan.cpp PROPERTIES
COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
target_compile_definitions(bbgpu PRIVATE IMGUI_IMPL_VULKAN_NO_PROTOTYPES
BB_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf")
set_source_files_properties(shim/bbport_overlay.cpp PROPERTIES
OBJECT_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf)
target_include_directories(bbgpu PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
OBJECT_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/third_party/fonts/DejaVuSans.ttf")
target_include_directories(bbgpu PRIVATE . shim layer games cp ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
if (WIN32)
target_include_directories(bbgpu PUBLIC platform/win32/include)
endif()
# Hunter's Atlas (shim/bbport_map*.cpp) reads its map packs with nlohmann/json (header only, MIT).
# bbport (Linux fork): the system package the online module (bbnet20) builds with.
find_package(nlohmann_json CONFIG REQUIRED)
target_link_libraries(bbgpu PRIVATE nlohmann_json::nlohmann_json)
target_compile_definitions(bbgpu PRIVATE BOOST_ASIO_STANDALONE NDEBUG)
target_compile_options(bbgpu PRIVATE -Wno-deprecated-declarations -Wno-missing-field-initializers)
# Loaded with bb-probe at startup (DT_NEEDED, never dlopen): thread_local access without a
# TLS descriptor call (_dl_tlsdesc_return was ~2% of the draw recording thread).
target_compile_options(bbgpu PRIVATE -ftls-model=initial-exec)
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against bb-probe.
target_link_options(bbgpu PRIVATE -Wl,--allow-shlib-undefined)
if (NOT WIN32)
target_compile_options(bbgpu PRIVATE -ftls-model=initial-exec)
# Runtime symbols (runtime_memory_*, runtime_process_*) resolve against bb-probe.
target_link_options(bbgpu PRIVATE -Wl,--allow-shlib-undefined)
endif()
target_link_libraries(bbgpu PRIVATE ffx-vulkan::portable ffx-vulkan::fsr4-v07-vulkan)
# Trophy names: trophy00.trp decrypted with the user's trophy key (shim/bbport_trophies.cpp).
# Without OpenSSL the unlocks are kept and shown by number.
find_package(OpenSSL QUIET COMPONENTS Crypto)
if (OpenSSL_FOUND)
target_link_libraries(bbgpu PRIVATE OpenSSL::Crypto)
target_compile_definitions(bbgpu PRIVATE BB_TROPHY_DECRYPT=1)
else()
message(STATUS "Trophy names: off (OpenSSL's libcrypto missing; unlocks shown by number)")
endif()
target_link_libraries(bbgpu PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic_enum::magic_enum
tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::X11 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit)
tsl::robin_map GPUOpen::VulkanMemoryAllocator PkgConfig::XXHASH PkgConfig::SDL3 PkgConfig::FFMPEG miniz::miniz Zydis::Zydis sirit)
if (NOT WIN32)
target_link_libraries(bbgpu PUBLIC PkgConfig::X11)
endif()
# Windows: the runtime (src/, C) and this library in one bb-probe.exe (on Linux build.sh links the
# runtime against libbbgpu.so). Below 1 TiB stay the host heap and thread stacks the game packs
# into 40-bit fields: no high-entropy address randomization.
if (WIN32)
file(GLOB BB_RUNTIME_SOURCES CONFIGURE_DEPENDS ../src/runtime*.c)
file(GLOB BB_ATRAC9_SOURCES CONFIGURE_DEPENDS ../third_party/LibAtrac9/C/src/*.c)
add_library(bbatrac9 STATIC ${BB_ATRAC9_SOURCES})
target_compile_options(bbatrac9 PRIVATE -w)
add_executable(bb-probe ../src/probe.c ../src/vulkan_smoke.c ${BB_RUNTIME_SOURCES}
platform/win32/bb-probe.rc)
set_target_properties(bb-probe PROPERTIES C_STANDARD 11 C_EXTENSIONS ON)
target_include_directories(bb-probe PRIVATE .. ../src)
target_compile_options(bb-probe PRIVATE $<$<COMPILE_LANGUAGE:C>:-Wall -Wextra>)
target_link_libraries(bb-probe PRIVATE bbgpu bbatrac9 pthread ws2_32 bcrypt dbghelp winmm onecore
${Vulkan_LIBRARIES})
# Plain -l names in the packages' CMake files (Zydis: Zycore) are found in the sysroot.
foreach(root ${CMAKE_FIND_ROOT_PATH})
target_link_directories(bb-probe PRIVATE ${root}/lib)
endforeach()
target_link_options(bb-probe PRIVATE -Wl,--disable-high-entropy-va
-Wl,--stack,16777216 -Wl,--error-limit=0)
endif()
# Explicit test targets; neither launches the game nor needs a Vulkan device.
add_executable(trophy-test EXCLUDE_FROM_ALL ../tests/test_trophies.cpp shim/bbport_trophy_data.cpp)
target_compile_options(trophy-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(trophy-test PRIVATE shim)
add_executable(motion-history-test EXCLUDE_FROM_ALL ../tests/test_motion_history.cpp)
target_compile_options(motion-history-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(motion-history-test PRIVATE ..)
add_executable(motion-shader-test EXCLUDE_FROM_ALL ../tests/test_motion_shaders.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(motion-shader-test PRIVATE shim shadps4 third_party/gcn/include)
target_include_directories(motion-shader-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
target_link_libraries(motion-shader-test PRIVATE bbgpu)
add_executable(ui-composition-test EXCLUDE_FROM_ALL ../tests/test_ui_composition.cpp)
target_compile_options(ui-composition-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(ui-composition-test PRIVATE ..)
add_executable(cp-decoder-test EXCLUDE_FROM_ALL ../tests/test_cp_decoder.cpp cp/cp_decoder.cpp)
target_compile_options(cp-decoder-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(cp-decoder-test PRIVATE cp)
add_executable(fx-init-clear-test EXCLUDE_FROM_ALL ../tests/test_fx_init_clear.cpp cp/cp_decoder.cpp)
target_compile_options(fx-init-clear-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(fx-init-clear-test PRIVATE . shim)
add_executable(effect-flags-test EXCLUDE_FROM_ALL ../tests/test_effect_flags.cpp)
target_compile_options(effect-flags-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(effect-flags-test PRIVATE shim)
add_executable(settings-test EXCLUDE_FROM_ALL ../tests/test_settings.cpp shim/bbport_settings.cpp)
target_compile_options(settings-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(settings-test PRIVATE .. shim)
# The clip-space matrices DLSS Frame Generation gets (upscale/camera_matrices.h): no GPU.
add_executable(camera-matrices-test EXCLUDE_FROM_ALL ../tests/test_camera_matrices.cpp)
target_compile_options(camera-matrices-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(camera-matrices-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 shadps4 third_party/gcn/include)
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
# from vk_platform.cpp. Keep them local and use an explicit dispatcher in the test.
target_compile_options(scene-resolution-test PRIVATE -UNDEBUG -fPIC)
@@ -206,7 +346,7 @@ set_source_files_properties(../tests/test_scene_resolution.cpp PROPERTIES
COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
target_link_libraries(scene-resolution-test PRIVATE bbgpu)
add_executable(taa-shader-test EXCLUDE_FROM_ALL ../tests/test_taa_shader.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(taa-shader-test PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include)
target_include_directories(taa-shader-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
target_compile_options(taa-shader-test PRIVATE -UNDEBUG -fPIC)
set_source_files_properties(../tests/test_taa_shader.cpp PROPERTIES
COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
@@ -214,11 +354,27 @@ target_link_libraries(taa-shader-test PRIVATE bbgpu)
add_dependencies(taa-shader-test host_shaders)
add_executable(camera-motion-test EXCLUDE_FROM_ALL ../tests/test_camera_motion.cpp ../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(camera-motion-test PRIVATE shim shadps4 ${SHADER_INCLUDE} third_party/gcn/include)
target_include_directories(camera-motion-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
target_compile_options(camera-motion-test PRIVATE -UNDEBUG -fPIC)
target_link_libraries(camera-motion-test PRIVATE bbgpu)
add_dependencies(camera-motion-test host_shaders)
add_executable(translation-memory-test EXCLUDE_FROM_ALL ../tests/test_translation_memory.cpp
../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(translation-memory-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include)
target_compile_options(translation-memory-test PRIVATE -UNDEBUG -fPIC)
set_source_files_properties(../tests/test_translation_memory.cpp PROPERTIES
COMPILE_OPTIONS "-fvisibility=hidden;-fvisibility-inlines-hidden")
target_link_libraries(translation-memory-test PRIVATE bbgpu)
# Spare images on a real GPU (textures/image_spares.h): the time a texture costs the recording
# thread with and without them, the GPU busy.
add_executable(image-spares-test EXCLUDE_FROM_ALL ../tests/test_image_spares.cpp
textures/image_spares.cpp vulkan/vk_common.cpp)
target_include_directories(image-spares-test PRIVATE . shim)
target_compile_options(image-spares-test PRIVATE -UNDEBUG)
target_link_libraries(image-spares-test PRIVATE Vulkan::Headers GPUOpen::VulkanMemoryAllocator)
# bbport: FSR 4 benchmark outside the game (tools/fsr4_bench.cpp): ninja -C out/gpu fsr4-bench.
add_executable(upscaler-support-test EXCLUDE_FROM_ALL ../tests/test_upscaler_support.cpp
shim/bbport_settings.cpp)
@@ -226,6 +382,49 @@ target_include_directories(upscaler-support-test PRIVATE shim)
target_compile_options(upscaler-support-test PRIVATE -UNDEBUG)
add_executable(fsr4-bench EXCLUDE_FROM_ALL tools/fsr4_bench.cpp
shadps4/video_core/renderer_vulkan/fsr411/fsr411.cpp)
target_include_directories(fsr4-bench PRIVATE shadps4/video_core/renderer_vulkan/fsr411)
upscale/fsr411/fsr411.cpp)
target_include_directories(fsr4-bench PRIVATE upscale/fsr411)
target_link_libraries(fsr4-bench PRIVATE ffx-vulkan::fsr4-v07-vulkan ffx-vulkan::fsr4-v07-assets Vulkan::Vulkan)
# bbport online module (gpu/bbnet20, libbbnet20.so): built when its libraries are there (shell.nix);
# BB_NET=OFF leaves it out. Without it the port plays offline.
option(BB_NET "Build the online module (shadNet)" ON)
if (BB_NET)
find_package(Protobuf CONFIG QUIET)
find_package(nlohmann_json CONFIG QUIET)
find_package(OpenSSL QUIET)
find_package(ZLIB QUIET)
find_path(MINIUPNPC_INCLUDE miniupnpc.h PATH_SUFFIXES miniupnpc)
find_library(MINIUPNPC_LIBRARY NAMES miniupnpc)
if (Protobuf_FOUND AND nlohmann_json_FOUND AND OpenSSL_FOUND AND ZLIB_FOUND AND
MINIUPNPC_INCLUDE AND MINIUPNPC_LIBRARY)
add_subdirectory(bbnet20)
message(STATUS "Online module: on (libbbnet20.so)")
else()
# Named one by one (#145: "the build has no online module" with no hint why).
set(missing "")
foreach(dep Protobuf nlohmann_json OpenSSL ZLIB)
if (NOT ${dep}_FOUND)
list(APPEND missing ${dep})
endif()
endforeach()
if (NOT MINIUPNPC_INCLUDE OR NOT MINIUPNPC_LIBRARY)
list(APPEND missing miniupnpc)
endif()
list(JOIN missing ", " missing)
message(STATUS "Online module: off (missing: ${missing}; shell.nix lists the packages)")
endif()
endif()
# bbport: the shader corpus replay (render/shader_capture.h): ninja -C out/gpu shader-corpus-test.
add_executable(shader-corpus-test EXCLUDE_FROM_ALL ../tests/test_shader_corpus.cpp
../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(shader-corpus-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
target_compile_options(shader-corpus-test PRIVATE -UNDEBUG -fPIC)
target_link_libraries(shader-corpus-test PRIVATE bbgpu)
add_executable(srt-walker-fault-test EXCLUDE_FROM_ALL ../tests/test_srt_walker_fault.cpp
../tests/test_gpu_runtime_stubs.cpp)
target_include_directories(srt-walker-fault-test PRIVATE . shim ${SHADER_INCLUDE} third_party/gcn/include third_party/half/include)
target_compile_options(srt-walker-fault-test PRIVATE -UNDEBUG -Wall -Wextra)
target_link_libraries(srt-walker-fault-test PRIVATE bbgpu)
+14 -10
View File
@@ -1,10 +1,14 @@
Vendored from https://github.com/shadps4-emu/shadPS4 commit b4e7ae776071ceb1dd35004577a8719481e1d258 (GPL-2.0-or-later).
Directories: src/video_core, src/shader_recompiler, src/common, src/core/libraries/{gnmdriver,videoout}, externals/gcn, externals/sirit, externals/half.
Also: src/core/libraries/kernel/{orbis_error,posix_error,time,process,equeue}.h, equeue.cpp, src/core/libraries/error_codes.h, src/core/platform.h, src/core/libraries/system/{userservice,systemservice}.h.
Also: src/core/libraries/avplayer (without the handle streamer), src/core/libraries/videodec/video_utils.{h,cpp}.
Local changes are marked with "bbport:" comments.
Backported from upstream after b4e7ae7 (2026-10-02), recompiler correctness fixes only:
233562929 V_INTERP_MOV_F32 P10/P20 selectors (#5087), d96278e1e compute shader floating-point
mode (#5126), e518a6515 S_SUBB_U32 SCC wrap (#5031), 282e5c792 S_ADDC_U32 SCC carry-out (#5030),
fb6367d29 32-bit literals in the high dword of 64-bit floats (#5114), 94e217781 V_BFM_B32 (#5185),
d1cb90dbb ADDC (#5200), dd43d1cac V_CVT_PK_U8_F32 (#5194), ef08b1e1f V_ALIGNBIT/ALIGNBYTE (#5186).
The GPU layer (gpu/) is the port's own code: command processor (cp/), GCN hardware definitions
(gcn/), shader translator GCN -> SPIR-V (shader/), renderer (render/), buffer and texture caches
(buffers/, textures/), memory tracking (memory/), platform libraries (platform/), upscalers
(upscale/). It started from shadPS4 (https://github.com/shadps4-emu/shadPS4, commit
b4e7ae776071ceb1dd35004577a8719481e1d258, GPL-2.0-or-later) and was rewritten part by part;
gpu/shadps4 was removed on 2026-10-13. The shader translator was checked against the shadPS4
translator it replaced on a corpus of the game's shaders (byte-identical SPIR-V,
tests/test_shader_corpus.cpp).
The online module (gpu/bbnet20) keeps shadPS4 v0.20.0's network, NP and shadNet code
(bbnet20/vendor/shadps4, SHADNET-PORT.md).
Third-party libraries in third_party/: sirit (SPIR-V emitter), gcn (AMD GCN headers), half,
httplib, imgui, fonts, amd-fsr1 (FidelityFX headers used by shaders), fsr-vulkan.
+79 -3
View File
@@ -1,4 +1,6 @@
/* C interface of the GPU library (gpu/): shadPS4's Liverpool/Vulkan video core,
// SPDX-FileCopyrightText: Copyright 2026 bbport contributors
// SPDX-License-Identifier: GPL-2.0-or-later
/* C interface of the GPU library (gpu/): the command processor and Vulkan renderer,
* GnmDriver, VideoOut and kernel event queues, adapted to the native loader. */
#ifndef BBGPU_H
#define BBGPU_H
@@ -16,8 +18,6 @@ typedef struct {
} BbGpuConfig;
/* Registers kernel event queues (needed with or without graphics). */
void bbgpu_register_kernel(void);
/* Co-op: the game folder and param.sfo identity for the network and PSN libraries. */
void bbnet_configure(const char *app0, const char *serial, const char *title, const char *app_ver);
/* Creates window, Vulkan device, presenter and GPU command processor. */
int bbgpu_init(const BbGpuConfig *config);
/* Function for an imported NID ("NID#lib#mod"), or 0 when the GPU library does not provide it. */
@@ -32,6 +32,82 @@ int bbgpu_text_input_begin(const char *initial_utf8, const char *prompt_utf8);
int bbgpu_text_input_poll(char *out_utf8, uint64_t size);
/* 1 while the in-game settings menu is open: the game's pad input is held neutral. */
int bbgpu_overlay_captures_input(void);
/* Hunter's Atlas: the pad's buttons with what its tile or map took out (the game gets the rest);
* the buttons unchanged while it is off or not in use. */
uint32_t bbgpu_atlas_route_pad(uint32_t buttons, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry);
/* 1 while the game should be silent: bbport.ini mute_unfocused is on and the window does not
* have focus. Safe from any thread (the audio ports read it for every buffer). */
int bbgpu_audio_muted(void);
/* A trophy the game unlocked (runtime_services.c): the overlay's notice. grade: P, G, S, B or '-';
* icon: a TROPxxx.rgba file of scripts/trophies.py (u32 width, u32 height, RGBA), or NULL. */
void bbgpu_trophy_notice(int id, char grade, const char *name_utf8, const char *icon_path);
/* Mouse look (runtime_pad.c): the window takes the mouse when it is clicked (relative mode) and
* lets it go on F1, losing focus or the menu opening. take: motion (pixels) and wheel notches
* since the last call, reset; captured: 1 while the window holds it; enable: mouse_look=0 in
* bbport.ini keeps the window from taking it. */
void bbgpu_mouse_take(double *dx, double *dy, int *wheel_up, int *wheel_down);
int bbgpu_mouse_captured(void);
/* Asks the window to take the mouse (relative mode, as a click in the game does); the window
* thread does it. */
void bbgpu_mouse_capture(void);
void bbgpu_mouse_look_enable(int enabled);
/* A module's look at the window's events first (an SDL_Event and the SDL_Window, on the window
* thread): nonzero takes the event (the port and the game do not see it). Up to 4. */
typedef int (*BbGpuEventFilter)(const void *sdl_event, void *sdl_window);
void bbgpu_add_event_filter(BbGpuEventFilter filter);
/* A module's menu pointer: nonzero while the game shows a menu the mouse points into. The window
* then lets the mouse go and shows the cursor, and takes the mouse back after (as it had it).
* Up to 4; pointer_wanted: any of them says so. */
/* A video (AvPlayer) playing now; bbgpu_video_skip stops it (1 when one was stopped). */
int bbgpu_video_active(void);
/* The game shows its world (not a loading screen): the clock of pad recordings runs. */
int bbgpu_world_active(void);
/* Microseconds the world has been shown (the pad recording clock). */
unsigned long long bbgpu_world_us(void);
int bbgpu_video_skip(void);
typedef int (*BbGpuPointerQuery)(void);
void bbgpu_add_pointer_query(BbGpuPointerQuery query);
int bbgpu_pointer_wanted(void);
/* A module's changes to the pad state the game reads (runtime_pad.c, after the host's input and
* the scripted one): the buttons (PS4 bits); the sticks (lx ly rx ry) as they are. Up to 4. */
typedef void (*BbGpuPadFilter)(uint32_t *buttons, const uint8_t *sticks);
void bbgpu_add_pad_filter(BbGpuPadFilter filter);
void bbgpu_filter_pad(uint32_t *buttons, const uint8_t *sticks);
/* The shown frame's width / height (the display buffer's; the window letterboxes it). */
float bbgpu_frame_aspect(void);
/* A line of text over the game, near the bottom, until called with NULL (any thread). */
void bbgpu_overlay_banner(const char *utf8);
/* Trophies (gpu/shim/bbport_trophies.cpp): setup takes the game's sce_sys folder and the user
* folder (once); unlock keeps the trophy and announces it over the frame the first time, and
* returns the platinum unlocked along with it, else -1. */
void bbgpu_trophy_setup(const char *sce_sys_host_dir, const char *user_dir);
int bbgpu_trophy_unlock(int id);
/* Native mouse look: turns the game's camera (radians, applied at its next update); 0 when the
* camera hook is not in (another game version, BB_NATIVE_MOUSE=0) or the camera does not take the
* turns now (lock-on, menus): drive the right stick then. */
int bbgpu_camera_look(float yaw, float pitch);
/* The pipeline caches written by an earlier build, translated again for this one; called before
* the runtime reserves the guest's memory (render/shader_rebuild.h). */
int bbgpu_rebuild_shader_caches(void);
/* One cache directory (RebuildCaches' child where there is no fork: bb-probe --rebuild-shader-cache). */
int bbgpu_rebuild_shader_cache(const char *dir);
int bbgpu_predict_shaders_test(const char *cache_dir, const char *pack, const char *game_dir,
const char *out_dir, int jobs);
int bbgpu_merge_shader_metas(const char *cache_dir, const char *pack, const char *game_dir,
const char *meta_dir, const char *out_pack, const char *work_dir, int jobs);
int bbgpu_meta_reconstruct_test(const char *cache_dir, const char *pack, const char *game_dir);
int bbgpu_merge_shader_packs(const char *cache_dir, const char *pack_a, const char *pack_b,
const char *game_dir, const char *out_pack, const char *work_dir, int jobs);
/* Shader recipes (render/shader_rebuild.h): bb-probe --export-shader-recipes CACHE_DIR GAME_DIR OUT
* and --import-shaders CACHE_DIR PACK GAME_DIR MOTION_PARAMS MOTION_POSITIONS (run by the GPU
* library itself, before the guest's memory exists in that process). */
int bbgpu_export_shader_recipes(const char *cache_dir, const char *game_dir, const char *out_path);
int bbgpu_import_shader_recipes(const char *cache_dir, const char *pack, const char *game_dir,
unsigned long long motion_params, unsigned long long motion_positions);
/* 1 while the free camera (the community patch) is on: the mouse drives it through the right stick. */
int bbgpu_free_camera(void);
/* The game's current camera state (its projection parameters at +0x50), 0 until known. */
uint64_t bbgpu_camera_state(void);
/* Patches the loaded image before the game runs (image still writable): libGnm entry hooks. */
void bbgpu_patch_image(unsigned char *image, uint64_t size);
/* Number of symbols registered by the vendored libraries (diagnostics). */
+66
View File
@@ -0,0 +1,66 @@
# bbport online module, second generation: libbbnet20.so. shadPS4 v0.20.0's network, NP and
# shadNet sources (vendor/shadps4, 529ceb3, with vendor-changes.diff) and The Hunter's Dream
# backend (thd/, from droogie/bbhost), as Supermedo/Awaken put them together
# (SHADNET-PORT.md). The port's only online module (src/runtime_net.c loads it, BB_NET_MODULE
# overrides the path; the former shadp2p module gpu/bbnet was removed 2026-10-26). Hidden
# symbols and -Bsymbolic: its C++ never meets libbbgpu's.
set(BBPORT_VERSION "0.6-pre1")
set(BBNET_VENDOR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/shadps4")
set(SHADNET_PROTO_OUT20 "${CMAKE_CURRENT_BINARY_DIR}/shadnet_proto_gen")
file(MAKE_DIRECTORY "${SHADNET_PROTO_OUT20}")
# Cross builds (Windows from Linux) run the build machine's protoc, of the sysroot's version.
set(BBNET_PROTOC protobuf::protoc)
if (CMAKE_CROSSCOMPILING)
find_program(BBNET_HOST_PROTOC protoc NO_CMAKE_FIND_ROOT_PATH REQUIRED)
set(BBNET_PROTOC "${BBNET_HOST_PROTOC}")
endif()
add_custom_command(
OUTPUT "${SHADNET_PROTO_OUT20}/shadnet.pb.cc" "${SHADNET_PROTO_OUT20}/shadnet.pb.h"
COMMAND ${BBNET_PROTOC} "--proto_path=${BBNET_VENDOR}/shadnet"
"--cpp_out=${SHADNET_PROTO_OUT20}" "${BBNET_VENDOR}/shadnet/shadnet.proto"
DEPENDS "${BBNET_VENDOR}/shadnet/shadnet.proto")
file(GLOB_RECURSE BBNET20_SOURCES CONFIGURE_DEPENDS
vendor/shadps4/core/libraries/net/*.cpp vendor/shadps4/core/libraries/network/*.cpp
vendor/shadps4/core/libraries/np/*.cpp vendor/shadps4/core/net/*.cpp
vendor/shadps4/shadnet/*.cpp)
# Trophies, commerce and the profile and Facebook dialogs stay in the C runtime.
list(FILTER BBNET20_SOURCES EXCLUDE REGEX
"np_trophy|trophy_ui|np_sns_facebook_dialog|np_profile_dialog/|np_commerce/")
# BBSC, Bloodborne seamless co-op (GPL-2.0+): its game code and the port's stand-ins for
# shadPS4's UI (bbsc/bbsc_glue.cpp). Installed only for Bloodborne with BB_BBSC=1.
file(GLOB BBNET20_BBSC_SOURCES CONFIGURE_DEPENDS bbsc/core/*.cpp bbsc/bbsc_glue.cpp)
file(GLOB BBNET20_THD_SOURCES CONFIGURE_DEPENDS thd/*.cpp compat/common/*.cpp)
add_library(bbnet20 SHARED ${BBNET20_SOURCES} ${BBNET20_THD_SOURCES} ${BBNET20_BBSC_SOURCES}
"${SHADNET_PROTO_OUT20}/shadnet.pb.cc" bbnet_glue.cpp)
target_include_directories(bbnet20 PRIVATE include bbsc compat . .. ../shim "${BBNET_VENDOR}"
"${BBNET_VENDOR}/shadnet" "${SHADNET_PROTO_OUT20}" ../third_party/imgui ../third_party/httplib)
# The version the shadNet client reports to the server (BB_CLIENT_VERSION overrides it).
target_compile_definitions(bbnet20 PRIVATE BOOST_ASIO_STANDALONE NDEBUG
CPPHTTPLIB_OPENSSL_SUPPORT CPPHTTPLIB_ZLIB_SUPPORT BBNET_PORT_VERSION="${BBPORT_VERSION}")
target_compile_options(bbnet20 PRIVATE -fvisibility=hidden -fvisibility-inlines-hidden
-Wno-deprecated-declarations -Wno-missing-field-initializers)
target_link_libraries(bbnet20 PRIVATE nlohmann_json::nlohmann_json protobuf::libprotobuf
OpenSSL::SSL OpenSSL::Crypto ZLIB::ZLIB "${MINIUPNPC_LIBRARY}" Vulkan::Headers fmt::fmt
Boost::headers magic_enum::magic_enum)
if (WIN32)
# Windows: bbgpu is inside bb-probe.exe (static), so bbnet20.dll takes the few functions it
# needs (log, paths, guest code hooks) from the executable's import library, not a second copy
# of the GPU library. Next to bb-probe.exe as bin/bbnet20.dll (no "lib" prefix).
target_include_directories(bbnet20 PRIVATE $<TARGET_PROPERTY:bbgpu,INTERFACE_INCLUDE_DIRECTORIES>)
target_compile_definitions(bbnet20 PRIVATE $<TARGET_PROPERTY:bbgpu,INTERFACE_COMPILE_DEFINITIONS>)
# wepoll (piscisaureus/wepoll v1.5.8, BSD-2-Clause): epoll over AFD for shadPS4's host_net.
target_sources(bbnet20 PRIVATE ../third_party/wepoll/wepoll.c)
target_include_directories(bbnet20 PRIVATE ../third_party/wepoll)
set_target_properties(bb-probe PROPERTIES ENABLE_EXPORTS ON)
target_link_options(bb-probe PRIVATE -Wl,--export-all-symbols)
target_link_libraries(bbnet20 PRIVATE bb-probe ws2_32 iphlpapi crypt32 PkgConfig::SDL3)
set_target_properties(bbnet20 PROPERTIES PREFIX "" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
else()
target_link_options(bbnet20 PRIVATE -Wl,-Bsymbolic -Wl,--exclude-libs,ALL)
set_target_properties(bbnet20 PROPERTIES BUILD_RPATH "\$ORIGIN" INSTALL_RPATH "\$ORIGIN"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
target_link_libraries(bbnet20 PRIVATE bbgpu)
endif()
target_include_directories(bbnet20 PRIVATE "${MINIUPNPC_INCLUDE}")
+33
View File
@@ -0,0 +1,33 @@
# Native shadNet bridge
The network-only source in `gpu/bbnet20/vendor/shadps4/` is shadPS4's own, unchanged except
where marked `bbport`:
https://github.com/shadps4-emu/shadPS4/tree/v.0.20.0
(529ceb32f5546b820588f194b903cc39d08015fe): `src/core/libraries/net`, `network`, `np`
(without trophies, commerce and the profile and Facebook dialogs), `src/core/net` and
`src/shadnet`. These portions are GPL-2.0-or-later, with their original notices.
`third_party/httplib/httplib.h` and `third_party/wepoll/` retain their embedded
third-party license notices.
The bbport changes: the shadNet session starts once the game is loaded
(`NpManager::InitializeRuntime`, called by `bbnet_initialize`), and the client reports
itself to the server as this port (`BB_CLIENT_VERSION`), not as shadPS4. A server that does not answer
when the game starts (still starting, a VPN not up yet, a short outage) is tried again in the
background, every 2 s and up to every 30 s, as after a dropped connection; one that answers and
refuses the sign-in is not. An `exit()` with the network worker running no longer aborts.
The renderer, shaders and upscalers were not imported. `bbnet_glue.cpp` and `include/`
implement what these libraries need from the rest of shadPS4 (kernel threads, user
service, settings) on this project's runtime. Online play is off unless `BB_ONLINE=1`.
## The Hunter's Dream backend (`thd/`)
`thd/` is the co-op pool for The Hunter's Dream (`online_pool=huntersdream`, which sets
`BB_NET_BACKEND=thd`). It adapts droogie/bbhost's network side
(https://github.com/droogie/bbhost/tree/566184a74f071166299bb656c4af479e5bbe2771):
`src/hle/net.cpp`, `src/hle/np_matching2.cpp`, `src/hle/np_signaling.cpp`, the NP parts of
`src/hle/system.cpp`, and `src/net/{http,account,session,stun}.cpp`. These portions are
GPL-3.0-or-later; copyright remains with the bbhost contributors. With that pool the game's NP and
sceNet imports are these, ahead of the shadPS4 ones; the shadNet pool is unchanged. sceHttp stays
shadPS4's, with two `bbport` hooks that act only in that pool: `*.example.com` host-override keys
and the account token and ruleset headers.
+58
View File
@@ -0,0 +1,58 @@
/* bbport online module (libbbnet20.so, gpu/bbnet20): the PS4 network and PSN libraries with the
* shadNet client. A separate library the runtime loads only for online play (BB_ONLINE=1,
* src/runtime_net.c); offline the port never touches it, and a build without it runs offline.
* Its C++ inside is private (hidden symbols, -Bsymbolic): these two calls are all it exports. */
#ifndef BBNET_H
#define BBNET_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define BBNET_ABI_VERSION 2
/* What the runtime gives the module: the game's identity (param.sfo, its folder for
* sce_sys/npbind.dat) and guest threads for the NP libraries' callbacks into the game. */
typedef struct BbNetHost {
uint32_t abi; /* BBNET_ABI_VERSION */
const char *app0; /* the game's folder */
const char *serial; /* TITLE_ID */
const char *title; /* TITLE */
const char *app_ver; /* APP_VER */
/* 0 or a positive errno value; entry has the System V ABI (the game's code calls it). */
int32_t (*thread_spawn)(void **thread, void *(__attribute__((sysv_abi)) *entry)(void *),
void *argument, uint64_t stack_size, const char *name);
int32_t (*thread_join)(void *thread, void **result);
/* Ends the calling guest thread (pthread_exit of the NP libraries' threads); does not return. */
void (*thread_exit)(void *value);
/* Nonzero when [address, address+size) is mapped guest memory (safe to read). */
int (*memory_is_mapped)(uintptr_t address, uint64_t size);
} BbNetHost;
/* Registers the libraries; 0 on success, else the module cannot run (the game stays offline). */
typedef int (*BbNetInitFn)(const BbNetHost *host);
int bbnet_init(const BbNetHost *host);
/* Optional: the game's image is mapped and patched, before its first instruction runs (game
* modules of the online module install their hooks here: BBSC for Bloodborne). */
typedef void (*BbNetImageReadyFn)(uint64_t image_base, uint64_t image_size);
void bbnet_image_ready(uint64_t image_base, uint64_t image_size);
/* Optional: the game's image is mapped and relocated but not yet patched (modules that read the
* original code, such as the Hunter's Dream backend, look at it here). */
typedef void (*BbNetImageMappedFn)(uint8_t *image, uint64_t image_size);
void bbnet_image_mapped(uint8_t *image, uint64_t image_size);
/* Optional: the game is loaded, its first instruction is next (online sessions start here). */
typedef void (*BbNetStartFn)(void);
void bbnet_start(void);
/* The module's function for an imported NID ("NID#lib#mod"), or 0. */
typedef uintptr_t (*BbNetResolveFn)(const char *scoped_nid);
uintptr_t bbnet_resolve(const char *scoped_nid);
#ifdef __cplusplus
}
#endif
#endif
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport co-op: what the network and NP libraries from shadp2p need from the rest of
// bbport co-op: what shadPS4 0.20.0's network and NP libraries need from the rest of
// shadPS4, implemented on top of the bbport runtime, and their registration.
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
@@ -14,6 +15,7 @@
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <vector>
#ifdef _WIN32
@@ -26,10 +28,12 @@
#endif
#include "common/elf_info.h"
#include "common/logging/line.h"
#include "common/types.h"
#include "core/emulator_settings.h"
#include "core/bloodborne_re.h"
#include "core/memory.h"
#include "core/debugger.h"
#include "core/bbport_bbsc_kit.h" // Teefpyre (SrShadowy fork)
#include "core/libraries/kernel/kernel.h"
#include "core/libraries/kernel/memory.h"
#include "core/libraries/kernel/threads/pthread.h"
@@ -43,9 +47,8 @@
#include "core/libraries/network/http.h"
#include "core/libraries/network/http2.h"
#include "core/libraries/network/net.h"
#include "core/libraries/net/net.h"
#include "core/libraries/network/netctl.h"
#include "core/libraries/network/ssl.h"
#include "core/libraries/network/ssl2.h"
#include "core/libraries/np/np_auth.h"
#include "core/libraries/np/np_common.h"
@@ -55,15 +58,44 @@
#include "core/libraries/np/np_party.h"
#include "core/libraries/np/np_score/np_score.h"
#include "core/libraries/np/np_signaling/np_signaling.h"
#include "core/libraries/np/np_tus.h"
#include "core/libraries/np/np_tus/np_tus.h"
#include "core/libraries/np/np_utility/np_utility.h"
#include "core/libraries/np/np_web_api/np_web_api.h"
#include "core/libraries/np/np_web_api2/np_web_api2.h"
#include <algorithm>
#include <sys/mman.h>
#include <SDL3/SDL_events.h>
extern "C" {
int32_t runtime_thread_spawn(void** out, void* PS4_SYSV_ABI (*entry)(void*), void* argument,
uint64_t stack, const char* name);
int32_t runtime_thread_join_spawned(void* thread, void** result);
#include "bbgpu.h"
#include "common/memory_patcher.h"
#include "common/singleton.h"
#include "core/bbsc_cinematic.h"
#include "core/bbsc_persistence.h"
#include "core/bbsc_relay_client.h"
#include "core/bloodborne_re.h"
#include "core/cpu_patches.h"
#include "core/file_format/psf.h"
#include "games/game_profile.h"
#include "thd/thd.h"
#include "bbnet20/bbnet.h"
namespace {
/// The runtime's side (bbnet_init).
BbNetHost g_host{};
} // namespace
// The Hunter's Dream backend reads game structures (the player's position for the live map):
// only from mapped guest memory (bbport).
namespace BbEngine {
bool host_read_safe(const void* p, void* out, std::size_t n) {
if (!g_host.memory_is_mapped(reinterpret_cast<uintptr_t>(p), n)) {
return false;
}
std::memcpy(out, p, n);
return true;
}
} // namespace BbEngine
// ---- Kernel ---------------------------------------------------------------------------------
@@ -278,15 +310,18 @@ int posix_pthread_create_name_np(PthreadT* thread, const PthreadAttrT* attr,
PthreadEntryFunc start, void* arg, const char* name) {
if (!thread || !start) return 22;
const u64 stack = attr && *attr ? (*attr)->stack : 0;
return runtime_thread_spawn(thread, start, arg, stack, name ? name : "np");
return g_host.thread_spawn(thread, start, arg, stack, name ? name : "np");
}
int posix_pthread_join(PthreadT thread, void** ret) {
return runtime_thread_join_spawned(thread, ret);
return g_host.thread_join(thread, ret);
}
void posix_pthread_exit(void* status) {
g_host.thread_exit(status);
}
} // namespace Libraries::Kernel
// ---- User and system service, debugger, Bloodborne helpers -------------------------------------
// ---- User and system service, debugger ----------------------------------------------------------
namespace Libraries::UserService {
s32 PS4_SYSV_ABI sceUserServiceGetInitialUser(int* user_id) {
@@ -315,38 +350,22 @@ s32 GetCurrentPid() {
}
} // namespace Core::Debugger
namespace Core::Bloodborne {
namespace {
std::mutex placement_mutex;
std::optional<std::string> placement;
} // namespace
std::optional<std::string> GetSeamlessHostPlacementHeader() {
std::scoped_lock lock{placement_mutex};
return placement;
}
bool SetSeamlessHostPlacementHeader(std::string_view value) {
if (value.empty() || value.size() > 4096) {
return false;
}
std::scoped_lock lock{placement_mutex};
placement = std::string{value};
return true;
}
void ClearSeamlessHostPlacementHeader() {
std::scoped_lock lock{placement_mutex};
placement.reset();
}
void TraceMatching2LeaveRoom(std::uintptr_t, std::uint64_t) {}
} // namespace Core::Bloodborne
// ---- Notifications (log only for now) ------------------------------------------------------------
// ---- Notifications ------------------------------------------------------------------------------
// The console and the log get every one; an Info notice (the login failed, the server is down or
// refuses this client) also shows for a while on the port's overlay line, over the game.
namespace ImGui::ShadNetNotify {
namespace {
std::mutex history_mutex;
std::vector<HistoryEntry> history;
std::size_t unread{};
// Bumped by every user of the overlay line: a notice clears it only while it is still its own.
std::atomic<u64> overlay_line_generation{0};
constexpr auto NoticeShown = std::chrono::seconds(15);
} // namespace
void OverlayLineTaken() {
++overlay_line_generation;
}
KindDisplay DisplayOf(Kind kind) {
switch (kind) {
case Kind::FriendRequest:
@@ -357,12 +376,26 @@ KindDisplay DisplayOf(Kind kind) {
return {"Friend", {0.9f, 0.4f, 0.4f, 1.0f}};
case Kind::Online:
return {"Online", {0.4f, 0.8f, 0.9f, 1.0f}};
case Kind::Warning:
return {"Warning", {0.95f, 0.6f, 0.2f, 1.0f}};
default:
return {"Info", {0.8f, 0.8f, 0.8f, 1.0f}};
}
}
void Push(Kind kind, std::string text) {
std::printf("Co-op: %s: %s\n", DisplayOf(kind).tag, text.c_str());
// One write of the whole line: the shadNet threads log at the same moment.
Common::Log::WriteLine(stdout,
std::string("Co-op: ") + DisplayOf(kind).tag + ": " + text + "\n");
if (kind == Kind::Info) {
const u64 mine = ++overlay_line_generation;
bbgpu_overlay_banner(text.c_str());
std::thread([mine] {
std::this_thread::sleep_for(NoticeShown);
if (overlay_line_generation.load() == mine) {
bbgpu_overlay_banner(nullptr);
}
}).detach();
}
std::scoped_lock lock{history_mutex};
history.push_back({kind, std::move(text), {}});
++unread;
@@ -398,10 +431,10 @@ void Register() {}
void Unregister() {}
} // namespace ImGui::InvitationPrompt
// ---- Game identity -------------------------------------------------------------------------------
// ---- Game identity -----------------------------------------------------------------------------
// ElfInfo lets shadPS4's Core::Emulator fill it in; this port has no emulator class, so the
// same name is used here for that one job.
// ElfInfo is filled in by shadPS4's Core::Emulator; this port has no emulator class, so the
// same name is used here for that one job (bbport).
namespace Core {
class Emulator {
public:
@@ -442,35 +475,217 @@ private:
};
} // namespace Core
extern "C" void bbnet_configure(const char* app0, const char* serial, const char* title,
const char* app_ver) {
Core::Emulator::SetGame(app0, serial, title, app_ver);
const auto& ids = Common::ElfInfo::Instance().GetNpCommIds();
std::printf("Co-op: %s, NP communication id %s%s\n", serial ? serial : "?",
ids.empty() ? "missing" : ids.front().c_str(),
EmulatorSettings.IsShadNetEnabled() ? "; online play on" : "");
// ---- The module's C interface (bbnet.h) -------------------------------------------------------
// The module's own symbol table (libbbgpu keeps another one: this library's symbols are its own,
// -Bsymbolic).
namespace {
struct Symbol {
std::string nid;
u64 address;
};
std::vector<Symbol> g_symbols;
} // namespace
namespace Core::Loader {
void SymbolsResolver::AddSymbol(const char* nid, const char*, const char*, SymbolType,
u64 address) {
g_symbols.push_back({nid, address});
}
} // namespace Core::Loader
#ifdef _WIN32
#define BBNET_EXPORT extern "C" __declspec(dllexport)
#else
#define BBNET_EXPORT extern "C" __attribute__((visibility("default")))
#endif
BBNET_EXPORT uintptr_t bbnet_resolve(const char* scoped_nid) {
const char* hash = std::strchr(scoped_nid, '#');
const size_t length = hash ? size_t(hash - scoped_nid) : std::strlen(scoped_nid);
for (const auto& symbol : g_symbols) {
if (symbol.nid.size() == length && !std::memcmp(symbol.nid.data(), scoped_nid, length)) {
return uintptr_t(symbol.address);
}
}
return 0;
}
// ---- Registration ------------------------------------------------------------------------------
BBNET_EXPORT void bbnet_image_mapped(uint8_t* image, uint64_t size) {
// The Hunter's Dream backend reads the game's code before the patches change it.
BbThd::SetImage(image, size);
}
extern "C" void bbnet_register(void) {
BBNET_EXPORT void bbnet_image_ready(uint64_t base, uint64_t size) {
MemoryPatcher::g_eboot_address = base;
MemoryPatcher::g_eboot_image_size = size;
// BBSC (Bloodborne seamless co-op): BB_BBSC=1 (the launcher's
// co-op switch). Its game code checks for CUSA03173 01.09; every retail Bloodborne runs the
// same 1.09 executable (gpu/games/bloodborne.cpp), and each of its sites is byte-checked.
const char* bbsc = std::getenv("BB_BBSC");
if (!bbsc || bbsc[0] != '1') {
return;
}
const Game::Profile* profile = Game::Active();
if (!profile || std::string_view{profile->name} != "Bloodborne") {
std::printf("BBSC: not Bloodborne: seamless co-op off\n");
return;
}
MemoryPatcher::g_game_serial = "CUSA03173";
Common::Singleton<PSF>::Instance()->app_ver = "01.09"; // [extra: Teefpyre]
extra_bbsc_setup_save_dir(); // [extra: Teefpyre]
// BBSC writes its byte patches straight into the game's code, and turns some on and off
// while the game runs (session teardown, the host's lamp): its code pages stay writable
// (shadPS4 maps them so; the port maps them read and execute).
unsigned writable = 0;
#ifdef _WIN32
// Windows: the image's executable regions from VirtualQuery, made writable through the
// runtime's mprotect (it keeps its own record of the guest's protections).
for (unsigned long long at = base; at < base + size;) {
MEMORY_BASIC_INFORMATION info{};
if (!VirtualQuery(reinterpret_cast<void*>(at), &info, sizeof(info)) || !info.RegionSize) {
break;
}
const unsigned long long start = std::max<unsigned long long>(
reinterpret_cast<unsigned long long>(info.BaseAddress), base);
const unsigned long long end = std::min<unsigned long long>(
reinterpret_cast<unsigned long long>(info.BaseAddress) + info.RegionSize, base + size);
constexpr DWORD Executable =
PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
if (info.State == MEM_COMMIT && (info.Protect & Executable) && start < end) {
writable += mprotect(reinterpret_cast<void*>(start), end - start,
PROT_READ | PROT_WRITE | PROT_EXEC) == 0;
std::printf("BBSC: code %#llx-%#llx writable\n", start, end);
}
at = reinterpret_cast<unsigned long long>(info.BaseAddress) + info.RegionSize;
}
#endif
if (FILE* maps = std::fopen("/proc/self/maps", "r")) {
char line[512];
while (std::fgets(line, sizeof(line), maps)) {
unsigned long long start = 0, end = 0;
char perms[5] = {};
if (std::sscanf(line, "%llx-%llx %4s", &start, &end, perms) != 3 || perms[2] != 'x') {
continue;
}
start = std::max<unsigned long long>(start, base);
end = std::min<unsigned long long>(end, base + size);
if (start < end) {
writable += mprotect(reinterpret_cast<void*>(start), end - start,
PROT_READ | PROT_WRITE | PROT_EXEC) == 0;
std::printf("BBSC: code %#llx-%#llx writable\n", start, end);
}
}
std::fclose(maps);
}
std::printf("BBSC: seamless co-op hooks for the image at %#llx (%u code mappings writable)\n",
static_cast<unsigned long long>(base), writable);
if (const char* probe = std::getenv("BB_BBSC_PROBE")) {
const unsigned long long at = std::strtoull(probe, nullptr, 0);
if (FILE* maps = std::fopen("/proc/self/maps", "r")) {
char line[512];
while (std::fgets(line, sizeof(line), maps)) {
unsigned long long a = 0, b = 0;
if (std::sscanf(line, "%llx-%llx", &a, &b) == 2 && at >= a && at < b) {
std::printf("BBSC probe: %s", line);
}
}
std::fclose(maps);
}
std::fflush(stdout);
std::printf("BBSC probe: byte %#x\n", unsigned(*reinterpret_cast<volatile u8*>(at)));
}
// The cinematic camera (F1, keyboard and mouse): BBSC sees the window's events first.
if (Core::Bbsc::SwitchEnabled("SHADPS4_BBSC_CINEMATIC")) {
bbgpu_add_event_filter([](const void* event, void* window) -> int {
return Core::Bbsc::CinematicHandleEvent(*static_cast<const SDL_Event*>(event),
static_cast<SDL_Window*>(window));
});
}
Core::Bloodborne::InstallSeamlessCoopPatches();
Core::Bbsc::InstallPersistentSessionPatches();
Core::Bloodborne::InstallReverseEngineeringTrace();
Core::Bbsc::StartRelayClient(MemoryPatcher::g_game_serial);
}
namespace Libraries::Np::NpManager {
void InitializeRuntime();
}
BBNET_EXPORT void bbnet_start(void) {
// The online session starts with the game (not at registration: the account and the
// game's SDK version are known only now).
if (BbThd::Enabled()) {
BbThd::Initialize();
return;
}
Libraries::Np::NpManager::InitializeRuntime();
}
BBNET_EXPORT int bbnet_init(const BbNetHost* host) {
if (!host || host->abi != BBNET_ABI_VERSION || !host->thread_spawn || !host->thread_join ||
!host->thread_exit || !host->memory_is_mapped) {
std::printf("Online: the runtime and the online module are from different versions\n");
return -1;
}
g_host = *host;
Core::Emulator::SetGame(host->app0, host->serial, host->title, host->app_ver);
// What BBSC reads of the game (param.sfo, the serial: shadPS4's PSF and memory patcher).
{
auto* psf = Common::Singleton<PSF>::Instance();
psf->app_ver = host->app_ver ? host->app_ver : "";
psf->title_id = host->serial ? host->serial : "";
psf->title = host->title ? host->title : "";
MemoryPatcher::g_game_serial = host->serial ? host->serial : "";
}
// BBSC patches the game's code, some of it while the game runs: from now on the port's own
// code patches leave it writable (bbnet_image_ready).
if (const char* bbsc = std::getenv("BB_BBSC"); bbsc && bbsc[0] == '1') {
Core::KeepGuestCodeWritable();
}
const auto& ids = Common::ElfInfo::Instance().GetNpCommIds();
std::string line = std::string("Co-op: ") + (host->serial ? host->serial : "?") +
", NP communication id " +
(ids.empty() ? std::string("missing") : ids.front());
if (EmulatorSettings.IsConnectedToNetwork()) {
line += "; online play on"; // a launcher can look for this
}
Common::Log::WriteLine(stdout, line + "\n"); // one write (line.h)
Core::Loader::SymbolsResolver resolver;
Core::Loader::SymbolsResolver* sym = &resolver;
Libraries::Net::RegisterLib(sym);
Libraries::NetCtl::RegisterLib(sym);
Libraries::Http::RegisterLib(sym);
Libraries::Http2::RegisterLib(sym);
Libraries::Ssl::RegisterLib(sym);
Libraries::Ssl2::RegisterLib(sym);
Libraries::Np::NpAuth::RegisterLib(sym);
Libraries::Np::NpCommon::RegisterLib(sym);
Libraries::Np::NpManager::RegisterLib(sym);
Libraries::Np::NpMatching2::RegisterLib(sym);
Libraries::Np::NpPartner::RegisterLib(sym);
Libraries::Np::NpParty::RegisterLib(sym);
Libraries::Np::NpScore::RegisterLib(sym);
Libraries::Np::NpSignaling::RegisterLib(sym);
Libraries::Np::NpTus::RegisterLib(sym);
Libraries::Np::NpWebApi::RegisterLib(sym);
Libraries::Np::NpWebApi2::RegisterLib(sym);
if (BbThd::Enabled()) {
// The Hunter's Dream: its NP and sceNet imports; the game's HTTP stays sceHttp's.
BbThd::Register(sym);
Libraries::Http::RegisterLib(sym);
Libraries::Http2::RegisterLib(sym);
Libraries::Ssl2::RegisterLib(sym);
} else {
Libraries::Net::RegisterLib(sym);
Libraries::NetCtl::RegisterLib(sym);
Libraries::Http::RegisterLib(sym);
Libraries::Http2::RegisterLib(sym);
// libSceSsl: not registered, as in shadPS4 (its loading is behind a title workaround).
Libraries::Ssl2::RegisterLib(sym);
Libraries::Np::NpAuth::RegisterLib(sym);
Libraries::Np::NpCommon::RegisterLib(sym);
Libraries::Np::NpManager::RegisterLib(sym);
Libraries::Np::NpMatching2::RegisterLib(sym);
Libraries::Np::NpPartner::RegisterLib(sym);
Libraries::Np::NpParty::RegisterLib(sym);
Libraries::Np::NpScore::RegisterLib(sym);
Libraries::Np::NpSignaling::RegisterLib(sym);
Libraries::Np::NpTus::RegisterLib(sym);
Libraries::Np::NpUtility::RegisterLib(sym);
Libraries::Np::NpWebApi::RegisterLib(sym);
Libraries::Np::NpWebApi2::RegisterLib(sym);
}
std::printf("Online: %zu network and PSN functions (%s)\n", g_symbols.size(),
BbThd::Enabled() ? "The Hunter's Dream" : "shadNet");
return 0;
}
// The Hunter's Dream backend starts its dispatcher as a guest thread (thd_session.cpp).
extern "C" int32_t runtime_thread_spawn(void** out, void* PS4_SYSV_ABI (*entry)(void*),
void* argument, uint64_t stack, const char* name) {
return g_host.thread_spawn(out, entry, argument, stack, name);
}
+30
View File
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport online module: what BBSC (shadps4/core/bloodborne_re.cpp, GPL-2.0+)
// needs from shadPS4's UI. Its loading screen is a line in the port's overlay; its animated title
// card (off by default in BBSC too) is not ported.
#include <cstdio>
#include <string_view>
#include "bbgpu.h"
#include "imgui/bbsc_intro_layer.h"
#include "imgui/bbsc_loading_layer.h"
#include "imgui/shadnet_notifications_layer.h"
namespace Core::Bbsc {
void ShowLoadingScreen(LoadingScreenKind kind) {
// The port's overlay line (BBSC draws its own loading screen in shadPS4's ImGui layer).
ImGui::ShadNetNotify::OverlayLineTaken();
bbgpu_overlay_banner(kind == LoadingScreenKind::Summon ? "Joining the host's world..."
: "Following the host...");
}
void HideLoadingScreen() {
ImGui::ShadNetNotify::OverlayLineTaken();
bbgpu_overlay_banner(nullptr);
}
void StartIntro(std::string_view) {}
} // namespace Core::Bbsc
+241
View File
@@ -0,0 +1,241 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "bbport_bbsc_kit.h"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <string_view>
#include <thread>
#include <sys/uio.h>
#include <unistd.h>
extern "C" int runtime_memory_region(std::uintptr_t address, std::uintptr_t* start,
std::uintptr_t* end, int* mapped);
extern "C" void runtime_memory_read_backing(std::uintptr_t address, void* data,
std::uint64_t size);
extern "C" const char* extra_bbsc_save_root(void) {
const char* dir = std::getenv("BB_GPU_USER_DIR");
if (dir == nullptr || dir[0] == '\0') {
dir = std::getenv("BB_USER_DIR");
}
return dir != nullptr && dir[0] != '\0' ? dir : nullptr;
}
extern "C" void extra_bbsc_setup_save_dir(void) {
if (std::getenv("BBSC_SAVE_DIR")) {
return;
}
const char* user = extra_bbsc_save_root();
const std::filesystem::path save_root =
std::filesystem::path(user && user[0] ? user : "user") / "savedata";
// bbport (Linux fork): the most recently written save folder, not the first one listed (a
// second region's copy, an old one); on a first start, with none yet, the one the game will
// write (user 1, src/runtime_services.c; BB_SAVEDATA_TITLE from src/runtime_savedata.c) -
// without a folder the kit was never given.
std::filesystem::path newest;
std::filesystem::file_time_type newest_time{};
std::error_code error;
for (const auto& id : std::filesystem::directory_iterator(save_root, error)) {
std::error_code title_ec;
for (const auto& title : std::filesystem::directory_iterator(id.path(), title_ec)) {
const auto folder = title.path() / "SPRJ0005";
std::error_code ec;
const auto written = std::filesystem::last_write_time(folder, ec);
if (!ec && std::filesystem::is_directory(folder, ec) &&
(newest.empty() || written > newest_time)) {
newest = folder;
newest_time = written;
}
}
}
const char* title = std::getenv("BB_SAVEDATA_TITLE");
if (newest.empty() && title != nullptr && title[0] != '\0' &&
std::strcmp(title, "UNKNOWN") != 0) {
newest = save_root / "1" / title / "SPRJ0005";
}
if (!newest.empty()) {
const std::string text = newest.string();
setenv("BBSC_SAVE_DIR", text.c_str(), 0);
std::printf("BBSC: save folder %s\n", text.c_str());
}
}
extern "C" void extra_bbsc_bell_scan_forever(std::atomic<bool>* ready, void (*prepare)(void)) {
if (ready == nullptr || prepare == nullptr) {
return;
}
int delay_s = 2;
while (!ready->load(std::memory_order_acquire)) {
prepare();
if (ready->load(std::memory_order_acquire)) {
break;
}
std::this_thread::sleep_for(std::chrono::seconds(delay_s));
if (delay_s < 4) {
++delay_s;
}
}
}
extern "C" bool extra_bbsc_check_mapping(const char* param, const char* line) {
if (param == nullptr || line == nullptr) {
return false;
}
if (param[1] != 'w' || std::strstr(line, ".so") != nullptr) {
return false;
}
return true;
}
extern "C" bool extra_safe_read(void* dst, const void* src, size_t size) {
if (dst == nullptr || src == nullptr || size == 0) {
return false;
}
std::memcpy(dst, src, size);
return true;
}
extern "C" void extra_bbsc_find_in_memory(
const std::uint8_t* needle_ptr, std::size_t needle_len,
std::uint64_t lo, std::uint64_t hi, std::size_t max_hits,
std::vector<std::uint64_t>* hits) {
if (!hits || !needle_ptr || needle_len == 0 || lo >= hi) {
return;
}
constexpr std::size_t Piece = 1u << 20;
std::vector<std::uint8_t> buffer(Piece + needle_len);
constexpr std::uint64_t GuestMin = 0x1000000000ULL;
auto match_chunk = [&](std::uint64_t base, std::size_t len) {
const std::uint8_t* data = buffer.data();
const std::uint8_t* last = data + len - needle_len;
for (const std::uint8_t* p = data; p <= last && hits->size() < max_hits; ++p) {
p = static_cast<const std::uint8_t*>(
std::memchr(p, needle_ptr[0], std::size_t(last - p) + 1));
if (!p) {
break;
}
if (std::size_t(p - data) < Piece &&
std::memcmp(p, needle_ptr, needle_len) == 0) {
hits->push_back(base + std::uint64_t(p - data));
}
}
};
// 1. Low mappings below GuestMin (e.g. eboot image): readable mappings from /proc/self/maps
if (lo < GuestMin) {
const std::uint64_t low_hi = std::min<std::uint64_t>(hi, GuestMin);
if (FILE* maps = std::fopen("/proc/self/maps", "r")) {
char line[512];
while (hits->size() < max_hits && std::fgets(line, sizeof(line), maps)) {
unsigned long long start = 0, end = 0;
char perms[5] = {};
if (std::sscanf(line, "%llx-%llx %4s", &start, &end, perms) != 3 || perms[0] != 'r') {
continue;
}
if (!extra_bbsc_check_mapping(perms, line)) {
continue;
}
start = std::max<unsigned long long>(start, lo);
end = std::min<unsigned long long>(end, low_hi);
for (std::uint64_t at = start; at < end && hits->size() < max_hits; at += Piece) {
const std::size_t want =
std::size_t(std::min<std::uint64_t>(end - at, Piece + needle_len - 1));
iovec local{buffer.data(), want};
iovec remote{reinterpret_cast<void*>(at), want};
ssize_t got = process_vm_readv(getpid(), &local, 1, &remote, 1, 0);
if (got < 0 && extra_safe_read(buffer.data(), reinterpret_cast<const void*>(at), want)) {
got = static_cast<ssize_t>(want);
}
if (got < ssize_t(needle_len)) {
continue;
}
match_chunk(at, std::size_t(got));
}
}
std::fclose(maps);
}
}
// 2. Guest mappings at or above GuestMin: query runtime_memory
std::uint64_t at = std::max<std::uint64_t>(lo, GuestMin);
while (at < hi && hits->size() < max_hits) {
std::uintptr_t r_start = 0, r_end = 0;
int mapped = 0;
if (!runtime_memory_region(at, &r_start, &r_end, &mapped) || r_end <= at) {
break;
}
if (!mapped) {
at = r_end;
continue;
}
const std::uint64_t start = std::max<std::uint64_t>(r_start, at);
const std::uint64_t end = std::min<std::uint64_t>(r_end, hi);
for (std::uint64_t chunk = start; chunk < end && hits->size() < max_hits; chunk += Piece) {
const std::size_t want =
std::size_t(std::min<std::uint64_t>(end - chunk, Piece + needle_len - 1));
if (want < needle_len) {
break;
}
runtime_memory_read_backing(chunk, buffer.data(), want);
match_chunk(chunk, want);
}
at = r_end;
}
}
extern "C" std::uint64_t extra_bbsc_get_solo_param_repo(
std::uint64_t image_base, const BbscParamMemory* mem) {
if (image_base == 0 || mem == nullptr || mem->read_u64 == nullptr || mem->readable == nullptr) {
return 0;
}
constexpr std::uint64_t kSoloRepoOffset = 0x05540340;
constexpr std::uint64_t kBasePtrOffset = 0x05537F58;
constexpr std::size_t kRepoMinSize = 64 * 0x48 + 0x38;
std::uint64_t repo = mem->read_u64(image_base + kSoloRepoOffset, 0);
if (!mem->readable(repo, kRepoMinSize)) {
const std::uint64_t base_ptr = mem->read_u64(image_base + kBasePtrOffset, 0);
if (mem->readable(base_ptr, 0x70)) {
repo = mem->read_u64(base_ptr, 0x68);
}
}
return mem->readable(repo, kRepoMinSize) ? repo : 0;
}
extern "C" void extra_bbsc_find_param_tables_direct(
std::uint64_t repo, const char* type, std::size_t type_len, std::uint16_t min_rows, std::uint32_t first_id,
const BbscParamMemory* mem, std::vector<std::uint64_t>* out_tables) {
if (repo == 0 || type == nullptr || mem == nullptr || out_tables == nullptr ||
mem->readable == nullptr || mem->read_s32 == nullptr || mem->read_u16 == nullptr ||
mem->read_u32 == nullptr || mem->read_u64 == nullptr || mem->match_name == nullptr) {
return;
}
constexpr std::size_t kRepoMinSize = 64 * 0x48 + 0x38;
if (!mem->readable(repo, kRepoMinSize)) {
return;
}
for (std::size_t idx = 0; idx < 64; ++idx) {
if (mem->read_s32(repo, idx * 0x48 + 0x28) <= 0) {
continue;
}
const std::uint64_t resource = mem->read_u64(repo, idx * 0x48 + 0x30);
const std::uint64_t holder = mem->readable(resource, 0x78) ? mem->read_u64(resource, 0x70) : 0;
const std::uint64_t table = mem->readable(holder, 0x78) ? mem->read_u64(holder, 0x70) : 0;
if (mem->readable(table, 0x60) && mem->match_name(table, type, type_len) &&
mem->read_u16(table, 0x0A) > min_rows &&
(first_id == 0 || mem->read_u32(table, 0x40) == first_id) &&
std::ranges::find(*out_tables, table) == out_tables->end()) {
out_tables->push_back(table);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <vector>
// Returns $BB_GPU_USER_DIR or $BB_USER_DIR when set and non-empty, else nullptr.
extern "C" const char* extra_bbsc_save_root(void);
// BBSC save directory automatic resolution across title IDs (CUSA00207, CUSA03173, etc.)
// [Teefpyre patch: dynamic save folder detection]
extern "C" void extra_bbsc_setup_save_dir(void);
// Hunter's Kit fix: the bell's param rows (goods + SpEffect) are scannable once the game
// data is mapped, but upstream's cap of 36 attempts (3 min) could expire if the user stayed
// at the title screen. This worker calls prepare every 2 s until ready turns true.
extern "C" void extra_bbsc_bell_scan_forever(std::atomic<bool>* ready, void (*prepare)(void));
// Safely reads memory even on DMABUF or protected pages, preventing SIGSEGV/SIGBUS.
extern "C" bool extra_safe_read(void* dst, const void* src, size_t size);
// Verifies if a memory mapping should be scanned (skips read-only and host .so libraries).
extern "C" bool extra_bbsc_check_mapping(const char* param, const char* line);
// Memory scan for BBSC param tables and symbols [Teefpyre patch / Linux BBSC Fix]:
// Handles low mappings (< GuestMin) via /proc/self/maps and guest mappings (>= GuestMin)
// via runtime_memory_read_backing.
extern "C" void extra_bbsc_find_in_memory(
const std::uint8_t* needle_ptr, std::size_t needle_len,
std::uint64_t lo, std::uint64_t hi, std::size_t max_hits,
std::vector<std::uint64_t>* hits);
// Memory access callbacks used by BBSC parameter operations
struct BbscParamMemory {
bool (*readable)(std::uint64_t addr, std::size_t size);
std::uint64_t (*read_u64)(std::uint64_t addr, std::size_t offset);
std::int32_t (*read_s32)(std::uint64_t addr, std::size_t offset);
std::uint16_t (*read_u16)(std::uint64_t addr, std::size_t offset);
std::uint32_t (*read_u32)(std::uint64_t addr, std::size_t offset);
bool (*match_name)(std::uint64_t table_addr, const char* name, std::size_t name_len);
};
// Returns the SoloParamRepository pointer if initialized and valid, else 0.
// Checks image_base + 0x05540340 and [[image_base + 0x05537F58] + 0x68].
// [Teefpyre patch: direct SoloParamRepository lookup without memory scanning]
extern "C" std::uint64_t extra_bbsc_get_solo_param_repo(
std::uint64_t image_base, const BbscParamMemory* mem);
// Finds param tables directly from SoloParamRepository without memory scanning.
extern "C" void extra_bbsc_find_param_tables_direct(
std::uint64_t repo, const char* type, std::size_t type_len, std::uint16_t min_rows, std::uint32_t first_id,
const BbscParamMemory* mem, std::vector<std::uint64_t>* out_tables);
+222
View File
@@ -0,0 +1,222 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
// Input side of the BBSC cinematic camera. While it is on, the keyboard and mouse fly the camera
// and never reach the game (shadPS4 would otherwise map them to the pad and move the hunter too);
// the controller is untouched. Keys:
// F1 on/off | hold right mouse: look | WASD move | E / Space up, Q / C down
// Shift fast, Alt slow | wheel: speed | Z / X zoom, V reset zoom | , . roll, / reset roll
// arrow keys: look | [ ] smoothing (glide) less / more
// Dead in co-op it watches a living hunter instead (bloodborne_re.cpp BbscWatchCamera): Tab / E /
// left click next hunter, Shift+Tab / Q previous, F fly freely and back; the controller's R1 / L1
// and right stick reach it only then (CinematicPadRead - the F1 camera leaves the pad to the hunter).
#include <algorithm>
#include <cmath>
#include <mutex>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_mouse.h>
#include "common/logging/log.h"
#include "core/bbsc_cinematic.h"
#include "core/bbsc_persistence.h"
namespace Core::Bbsc {
namespace {
std::mutex cine_mutex;
CinematicInput cine_state;
bool looking = false;
bool watching = false; // the game thread says the camera watches a hunter
unsigned pad_last = 0; // pad buttons at the last read
int pad_switch = 0; // R1 / L1 presses while watching
float pad_rx = 0.0f, pad_ry = 0.0f, pad_ly = 0.0f;
float PadAxis(unsigned char value) {
const float v = (static_cast<float>(value) - 128.0f) / 127.0f;
constexpr float Dead = 0.15f;
return std::abs(v) < Dead ? 0.0f
: std::clamp((v - std::copysign(Dead, v)) / (1.0f - Dead), -1.0f, 1.0f);
}
bool CinematicEnabledBySwitch() {
static const bool enabled = SwitchEnabled("SHADPS4_BBSC_CINEMATIC");
return enabled;
}
void SetKey(SDL_Keycode key, bool down) {
switch (key) {
case SDLK_W: cine_state.forward = down; break;
case SDLK_S: cine_state.back = down; break;
case SDLK_A: cine_state.left = down; break;
case SDLK_D: cine_state.right = down; break;
case SDLK_E:
case SDLK_SPACE: cine_state.up = down; break;
case SDLK_Q:
case SDLK_C: cine_state.down = down; break;
case SDLK_LSHIFT:
case SDLK_RSHIFT: cine_state.fast = down; break;
case SDLK_LALT:
case SDLK_RALT: cine_state.slow = down; break;
case SDLK_Z: cine_state.zoom_in = down; break;
case SDLK_X: cine_state.zoom_out = down; break;
case SDLK_V: cine_state.zoom_reset = cine_state.zoom_reset || down; break;
case SDLK_COMMA: cine_state.roll_left = down; break;
case SDLK_PERIOD: cine_state.roll_right = down; break;
case SDLK_SLASH: cine_state.roll_reset = cine_state.roll_reset || down; break;
case SDLK_UP: cine_state.look_up = down; break;
case SDLK_DOWN: cine_state.look_down = down; break;
case SDLK_LEFT: cine_state.look_left = down; break;
case SDLK_RIGHT: cine_state.look_right = down; break;
case SDLK_LEFTBRACKET:
if (down) --cine_state.smooth_steps;
break;
case SDLK_RIGHTBRACKET:
if (down) ++cine_state.smooth_steps;
break;
default: break;
}
}
void ReleaseAll(SDL_Window* window) {
const bool enabled = cine_state.enabled;
cine_state = CinematicInput{};
cine_state.enabled = enabled;
if (looking && window != nullptr) {
SDL_SetWindowRelativeMouseMode(window, false);
}
looking = false;
}
} // namespace
bool CinematicHandleEvent(const SDL_Event& event, SDL_Window* window) {
if (!CinematicEnabledBySwitch()) {
return false;
}
std::scoped_lock lock{cine_mutex};
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_F1) {
if (!event.key.repeat) {
cine_state.enabled = !cine_state.enabled;
ReleaseAll(window);
LOG_INFO(Debug, "BBSC cinematic camera: {} (F1)", cine_state.enabled ? "on" : "off");
}
return true;
}
if (event.type == SDL_EVENT_KEY_UP && event.key.key == SDLK_F1) {
return true;
}
if (!cine_state.enabled) {
return false;
}
switch (event.type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP: {
const SDL_Keycode key = event.key.key;
if (key >= SDLK_F2 && key <= SDLK_F12) {
return false; // the emulator's own hotkeys (fullscreen, fps, pause...) keep working
}
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
if (key == SDLK_TAB) {
cine_state.switch_target += (event.key.mod & SDL_KMOD_SHIFT) != 0 ? -1 : 1;
} else if (key == SDLK_E) {
++cine_state.switch_target;
} else if (key == SDLK_Q) {
--cine_state.switch_target;
} else if (key == SDLK_F) {
cine_state.toggle_free = true;
}
}
SetKey(key, event.type == SDL_EVENT_KEY_DOWN);
return true;
}
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
if (event.button.button == SDL_BUTTON_LEFT && event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
++cine_state.switch_target;
}
if (event.button.button == SDL_BUTTON_RIGHT) {
looking = event.type == SDL_EVENT_MOUSE_BUTTON_DOWN;
if (window != nullptr) {
SDL_SetWindowRelativeMouseMode(window, looking);
}
}
return true;
case SDL_EVENT_MOUSE_MOTION:
if (looking) {
cine_state.mouse_dx += event.motion.xrel;
cine_state.mouse_dy += event.motion.yrel;
}
return true;
case SDL_EVENT_MOUSE_WHEEL:
cine_state.wheel += event.wheel.y > 0.0f ? 1 : (event.wheel.y < 0.0f ? -1 : 0);
return true;
case SDL_EVENT_WINDOW_FOCUS_LOST:
ReleaseAll(window);
return false;
default:
return false;
}
}
CinematicInput TakeCinematicInput() {
std::scoped_lock lock{cine_mutex};
CinematicInput out = cine_state;
if (watching) {
out.pad_look_x = pad_rx;
out.pad_look_y = -pad_ry;
out.pad_move_y = -pad_ly; // stick up = closer
out.switch_target += pad_switch;
}
pad_switch = 0;
cine_state.switch_target = 0;
cine_state.toggle_free = false;
cine_state.mouse_dx = 0.0f;
cine_state.mouse_dy = 0.0f;
cine_state.wheel = 0;
cine_state.smooth_steps = 0;
cine_state.zoom_reset = false;
cine_state.roll_reset = false;
return out;
}
void CinematicForceOff() {
std::scoped_lock lock{cine_mutex};
cine_state = CinematicInput{};
}
void CinematicForceOn() {
std::scoped_lock lock{cine_mutex};
if (!cine_state.enabled) {
cine_state = CinematicInput{};
cine_state.enabled = true;
}
}
void CinematicSetWatching(bool on) {
std::scoped_lock lock{cine_mutex};
if (watching != on) {
pad_switch = 0;
pad_rx = pad_ry = pad_ly = 0.0f;
}
watching = on;
}
void CinematicPadRead(unsigned buttons, unsigned char rx, unsigned char ry, unsigned char lx,
unsigned char ly) {
(void)lx;
std::scoped_lock lock{cine_mutex};
constexpr unsigned L1 = 0x400, R1 = 0x800;
const unsigned pressed = buttons & ~pad_last;
pad_last = buttons;
if (!cine_state.enabled || !watching) {
return;
}
pad_switch += ((pressed & R1) != 0 ? 1 : 0) - ((pressed & L1) != 0 ? 1 : 0);
pad_rx = PadAxis(rx);
pad_ry = PadAxis(ry);
pad_ly = PadAxis(ly);
}
} // namespace Core::Bbsc
+58
View File
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// BBSC cinematic camera (2026-09-28): a free camera for filming, flown with keyboard and mouse while
// the controller keeps playing the hunter. The window side (sdl_window.cpp) feeds it input; the
// game side (bloodborne_re.cpp, BbscCinematicCameraHook) turns the game's own debug free camera on
// and moves it. Switch SHADPS4_BBSC_CINEMATIC (default on). F1 toggles it.
union SDL_Event;
struct SDL_Window;
namespace Core::Bbsc {
// Window thread. True when the event belongs to the cinematic camera and must not reach the game.
bool CinematicHandleEvent(const SDL_Event& event, SDL_Window* window);
struct CinematicInput {
bool enabled = false;
bool forward = false, back = false, left = false, right = false, up = false, down = false;
bool fast = false, slow = false;
bool zoom_in = false, zoom_out = false, zoom_reset = false;
bool roll_left = false, roll_right = false, roll_reset = false;
bool look_up = false, look_down = false, look_left = false, look_right = false;
float mouse_dx = 0.0f; // accumulated since the last TakeCinematicInput
float mouse_dy = 0.0f;
int wheel = 0; // speed steps, accumulated
int smooth_steps = 0; // smoothing steps, accumulated
// Controller (mod only, -1..1, added to the keys): left stick move, right stick look, L1/R1
// down/up, L2/R2 zoom out/in.
float pad_move_x = 0.0f, pad_move_y = 0.0f, pad_rise = 0.0f;
float pad_look_x = 0.0f, pad_look_y = 0.0f, pad_zoom = 0.0f;
// Watching a living hunter while dead (2026-10-20): presses since the last call, + next /
// - previous (Tab, E, left click, R1 / Shift+Tab, Q, L1), and F (watch <-> fly freely).
int switch_target = 0;
bool toggle_free = false;
};
// Game thread: the current keys plus everything accumulated since the last call.
CinematicInput TakeCinematicInput();
// Game thread: the camera could not be switched on (logged); the toggle goes back to off.
void CinematicForceOff();
// Game thread: switches the camera on (you died in co-op); nothing when it is already on.
void CinematicForceOn();
// Game thread: the camera is watching a living hunter (you are dead). Emulator build: only then
// does the controller reach the camera (CinematicPadRead) - the F1 camera leaves it to the hunter.
void CinematicSetWatching(bool watching);
// Emulator build, pad thread (scePadReadState): the game's pad data, read only. While watching,
// R1 / L1 switch the hunter and the right stick turns the camera.
void CinematicPadRead(unsigned buttons, unsigned char rx, unsigned char ry, unsigned char lx,
unsigned char ly);
} // namespace Core::Bbsc
+191
View File
@@ -0,0 +1,191 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: BBSC's own texts in the game's language, see bbsc_i18n.h.
#include "core/bbsc_i18n.h"
#include <array>
#include <cstdlib>
#include <fstream>
#include <mutex>
#include <unordered_map>
namespace Core::Bbsc {
namespace {
// The languages of bbsc/translations, in the column order of bbsc_translations.inc.
enum Language { Spanish, French, German, Italian, Portuguese, Russian, LanguageCount };
struct Translation {
const char16_t* english;
const char16_t* text[LanguageCount];
};
constexpr Translation Translations[] = {
#include "core/bbsc_translations.inc"
};
constexpr std::u16string_view BossTag = u"<?npcNameId@";
constexpr std::u16string_view BossPlaceholder = u"{boss}";
// A text with numbers (BBSC 0.0.2's "Watching hunter 1 of 2."): its first number is {n}, its
// second {count}.
constexpr std::array<std::u16string_view, 2> NumberPlaceholders{u"{n}", u"{count}"};
// The language the game shows its text in: bbport.ini game_text_language (one of the game's
// codes, scripts/patches.py), else BB_LANGUAGE (the PS4 system language code). -1: English and
// the others.
int TextLanguageCode(const std::string& code) {
if (code == "eses" || code == "esar") {
return int{Spanish};
}
if (code == "frfr") {
return int{French};
}
if (code == "dede") {
return int{German};
}
if (code == "itit") {
return int{Italian};
}
if (code == "ptbr" || code == "ptpt") {
return int{Portuguese};
}
if (code == "ruru") {
return int{Russian};
}
return -1;
}
std::string IniTextLanguage() {
const char* path = std::getenv("BB_CONFIG");
std::ifstream ini{path != nullptr && *path != '\0' ? path : "bbport.ini"};
std::string line, value;
while (std::getline(ini, line)) {
if (line.rfind("game_text_language", 0) != 0) {
continue;
}
const size_t equals = line.find('=');
if (equals == std::string::npos ||
line.find_first_not_of(" \t", 18) != equals) {
continue;
}
value = line.substr(equals + 1);
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t\r") + 1);
}
return value;
}
int GameLanguage() {
static const int language = [] {
if (const std::string code = IniTextLanguage(); !code.empty() && code != "auto") {
return TextLanguageCode(code);
}
const char* value = std::getenv("BB_LANGUAGE");
switch (value != nullptr ? std::atoi(value) : 1) {
case 3: // Spanish (Spain)
case 20: // Spanish (Latin America)
return int{Spanish};
case 2: // French
case 22: // French (Canada)
return int{French};
case 4:
return int{German};
case 5:
return int{Italian};
case 7: // Portuguese (Portugal)
case 17: // Portuguese (Brazil)
return int{Portuguese};
case 8:
return int{Russian};
default:
return -1;
}
}();
return language;
}
const char16_t* Lookup(std::u16string_view english, int language) {
for (const Translation& entry : Translations) {
if (english == entry.english) {
return entry.text[language];
}
}
return nullptr;
}
std::u16string Replace(std::u16string text, std::u16string_view from, std::u16string_view to) {
for (size_t at = text.find(from); at != std::u16string::npos; at = text.find(from, at)) {
text.replace(at, from.size(), to);
at += to.size();
}
return text;
}
} // namespace
const char16_t* Text(const char16_t* english) {
const int language = GameLanguage();
if (english == nullptr || language < 0) {
return english;
}
if (const char16_t* text = Lookup(english, language)) {
return text;
}
// A text naming one boss: its {boss} form's translation, with the same tag. Made once per
// text and kept (the game keeps pointing at it).
const std::u16string_view view{english};
const size_t tag = view.find(BossTag);
const size_t tag_end = tag != std::u16string_view::npos ? view.find(u"?>", tag) : tag;
static std::mutex mutex;
static std::unordered_map<std::u16string, std::u16string> made;
std::scoped_lock lock{mutex};
if (const auto found = made.find(std::u16string{view}); found != made.end()) {
return found->second.c_str();
}
if (tag_end == std::u16string_view::npos) {
// A text with numbers: its {n} / {count} form's translation, with the same numbers. The
// text may sit in a buffer that is written again, so the key is a copy of it.
std::u16string form;
std::array<std::u16string, NumberPlaceholders.size()> numbers;
size_t count = 0;
for (size_t at = 0; at < view.size();) {
if (view[at] >= u'0' && view[at] <= u'9' && count < numbers.size()) {
size_t end = at;
while (end < view.size() && view[end] >= u'0' && view[end] <= u'9') {
++end;
}
numbers[count] = std::u16string{view.substr(at, end - at)};
form += NumberPlaceholders[count++];
at = end;
} else {
form += view[at++];
}
}
const char16_t* text = count != 0 ? Lookup(form, language) : nullptr;
if (text == nullptr) {
return english;
}
std::u16string out{text};
for (size_t i = 0; i < count; ++i) {
out = Replace(out, NumberPlaceholders[i], numbers[i]);
}
return made.emplace(std::u16string{view}, std::move(out)).first->second.c_str();
}
const std::u16string_view boss = view.substr(tag, tag_end + 2 - tag);
std::u16string form{view};
form.replace(tag, boss.size(), BossPlaceholder);
const char16_t* text = Lookup(form, language);
if (text == nullptr) {
return english;
}
return made.emplace(std::u16string{view}, Replace(text, BossPlaceholder, boss))
.first->second.c_str();
}
std::u16string TextWith(const char16_t* english_template, std::u16string_view boss) {
const int language = GameLanguage();
const char16_t* text = language >= 0 ? Lookup(english_template, language) : nullptr;
return Replace(text != nullptr ? text : english_template, BossPlaceholder, boss);
}
} // namespace Core::Bbsc
+22
View File
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: BBSC's own texts in the game's language. BBSC writes them in English; the translations
// (bbsc/translations/*.json, made into bbsc_translations.inc by gen_translations.py) are looked up
// by that English text, so BBSC's sources keep their English strings as they are.
#pragma once
#include <string>
#include <string_view>
namespace Core::Bbsc {
// `english` in the language the game shows its text in (bbport.ini game_text_language, else
// BB_LANGUAGE, the PS4 system language code), or `english`
// itself when there is no translation. A text naming one boss (<?npcNameId@N?>) uses the
// translation of its {boss} form. The pointer stays valid for the whole run.
const char16_t* Text(const char16_t* english);
// The translation of `english_template`, whose {boss} is replaced by `boss`.
std::u16string TextWith(const char16_t* english_template, std::u16string_view boss);
} // namespace Core::Bbsc
+100
View File
@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/bbsc_memscan.h"
#ifdef _WIN32
#include <windows.h>
#endif
#include <algorithm>
#include <cstdio>
#include <cstring>
#ifndef _WIN32
#include <sys/uio.h>
#include <unistd.h>
#include "core/bbport_bbsc_kit.h" // Teefpyre (SrShadowy fork)
#endif
namespace Core::Bbsc {
namespace {
#ifdef _WIN32
// One region. The emulator changes page protection all the time (GPU tracking), so a page that was
// readable a moment ago may fault now. bbport: clang/mingw has no __try, so the region is read in
// chunks with ReadProcessMemory (a chunk that no longer reads ends the region, no fault); chunks
// overlap by length - 1 bytes so a match across a boundary is found once.
void SearchRegion(const std::uint8_t* start, std::size_t size, const std::uint8_t* needle,
std::size_t length, std::vector<std::uint64_t>& hits, std::size_t max_hits) {
constexpr std::size_t Chunk = 1u << 20;
if (size < length) {
return;
}
std::vector<std::uint8_t> buffer(std::min(size, Chunk + length - 1));
std::size_t offset = 0;
while (hits.size() < max_hits && size - offset >= length) {
const std::size_t want = std::min(buffer.size(), size - offset);
SIZE_T read = 0;
if (!ReadProcessMemory(GetCurrentProcess(), start + offset, buffer.data(), want, &read) ||
read != want) {
return;
}
const std::uint8_t* end = buffer.data() + want;
const std::uint8_t* at = buffer.data();
while (hits.size() < max_hits && end - at >= static_cast<std::ptrdiff_t>(length)) {
at = static_cast<const std::uint8_t*>(
std::memchr(at, needle[0], static_cast<std::size_t>(end - at) - length + 1));
if (at == nullptr) {
break;
}
if (std::memcmp(at, needle, length) == 0) {
hits.push_back(reinterpret_cast<std::uint64_t>(start + offset + (at - buffer.data())));
}
++at;
}
if (want < buffer.size()) {
break;
}
offset += want - (length - 1);
}
}
#endif
} // namespace
std::vector<std::uint64_t> FindInMemory(std::span<const std::uint8_t> needle, std::uint64_t lo,
std::uint64_t hi, std::size_t max_hits) {
std::vector<std::uint64_t> hits;
#ifdef _WIN32
if (needle.empty()) {
return hits;
}
constexpr DWORD Readable = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ |
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
std::uint64_t at = lo;
while (at < hi && hits.size() < max_hits) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(reinterpret_cast<void*>(at), &info, sizeof(info)) == 0) {
break;
}
const auto start = reinterpret_cast<std::uint64_t>(info.BaseAddress);
const std::uint64_t end = start + info.RegionSize;
if (info.State == MEM_COMMIT && (info.Protect & Readable) != 0 &&
(info.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0) {
SearchRegion(reinterpret_cast<const std::uint8_t*>(start), info.RegionSize,
needle.data(), needle.size(), hits, max_hits);
}
if (end <= at) {
break;
}
at = end;
}
#else
extra_bbsc_find_in_memory(needle.data(), needle.size(), lo, hi, max_hits, &hits); // [extra: Teefpyre]
return hits; // [extra: Teefpyre]
#endif
return hits;
}
} // namespace Core::Bbsc
+21
View File
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
// BBSC: find bytes in the game's memory (the param tables are found by their header text). Kept
// out of bloodborne_re.cpp so <windows.h> stays out of it. Shared by the emulator and the mod DLL.
#pragma once
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace Core::Bbsc {
// Every address in [lo, hi) where `needle` sits in committed, readable memory (at most max_hits).
// Slow (a full walk of the address range): call it once, off the game thread.
std::vector<std::uint64_t> FindInMemory(std::span<const std::uint8_t> needle, std::uint64_t lo,
std::uint64_t hi, std::size_t max_hits);
} // namespace Core::Bbsc
+212
View File
@@ -0,0 +1,212 @@
// Generated by tools/gamedata/gen_messages.py from the game's English text - do not edit.
// {category, message id, BBSC text} // original text
{30, 102001, u"<?joinName?> joined your game."}, // The bell's resonance has beckoned <?joinName?>.
{30, 102003, u"<?joinName?> joined your game."}, // <?joinName?> has arrived by the bell's resonance
{30, 102004, u"<?joinName?> joined your game."}, // <?joinName?> has arrived by the bell's resonance
{30, 102005, u"<?joinName?> joined your game."}, // <?joinName?> has arrived by the bell's resonance
{30, 102006, u"<?joinName?> joined your game."}, // Confederate <?joinName?> has arrived by the bell's resonance.
{30, 102007, u"<?joinName?> joined your game."}, // Confederate <?joinName?> has arrived by the bell's resonance.
{30, 102056, u"<?joinName?> joined your game."}, // Confederate <?joinName?> has arrived by the bell's resonance.
{30, 102057, u"<?joinName?> joined your game."}, // Confederate <?joinName?> has arrived by the bell's resonance.
{30, 102051, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102052, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102053, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102054, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102055, u"<?joinName?> joined your game."}, // Old Hunter <?joinName?> has arrived by the bell's resonance.
{30, 102100, u"You joined <?hostName?>'s game."}, // Entered the world of <?hostName?>, who beckoned you here.
{30, 102200, u"You joined <?hostName?>'s game."}, // Entered the world of <?hostName?>.
{30, 102600, u"You joined <?hostName?>'s game."}, // You are now in the world of the beckoner, <?hostName?>.
{30, 102700, u"You joined <?hostName?>'s game."}, // You are now in the world of the beckoner, <?hostName?>.
{30, 103100, u"You died."}, // You died. Returning to your world.
{30, 103200, u"You died."}, // You died. Returning to your world.
{30, 103300, u"You died."}, // You died. Returning to your world.
{30, 103400, u"You died."}, // You died. Returning to your world.
{30, 103500, u"You died."}, // You died. Returning to your world.
{30, 103101, u"The host died.\nYou stay in their game."}, // Your beckoner has died. Returning to own world.
{30, 103001, u"<?deadName?> died.\nThey will respawn."}, // Cooperator <?deadName?> has died.
{30, 103006, u"<?deadName?> died.\nThey will respawn."}, // Confederate <?deadName?> has died.
{30, 103007, u"<?deadName?> died.\nThey will respawn."}, // Confederate <?deadName?> has died.
{30, 103056, u"<?deadName?> died.\nThey will respawn."}, // Confederate <?deadName?> has died.
{30, 103057, u"<?deadName?> died.\nThey will respawn."}, // Confederate <?deadName?> has died.
{30, 103051, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 103052, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 103053, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 103054, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 103055, u"<?deadName?> died.\nThey will respawn."}, // Old Hunter <?deadName?> has died.
{30, 106200, u"The host <?hostName?> died.\nYou stay in their game."}, // Beckoner <?hostName?> has fallen. It is time to bid farewell.
{30, 105200, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 105300, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 105400, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 105500, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 141010, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 141020, u"The host started the battle against\n<?npcNameId@106000?>."}, // Battle with <?npcNameId@106000?> has commenced. Returning to your world.
{30, 141030, u"The host started the battle against\n<?npcNameId@127000?>."}, // Battle with <?npcNameId@127000?> has commenced. Returning to your world.
{30, 141040, u"The host started the battle against\n<?npcNameId@209000?>."}, // Battle with <?npcNameId@209000?> has commenced. Returning to your world.
{30, 141050, u"The host started the battle against\n<?npcNameId@210000?>."}, // Battle with <?npcNameId@210000?> has commenced. Returning to your world.
{30, 141060, u"The host started the battle against\n<?npcNameId@212010?>."}, // Battle with <?npcNameId@212010?> has commenced. Returning to your world.
{30, 141070, u"The host started the battle against\n<?npcNameId@212020?>."}, // Battle with <?npcNameId@212020?> has commenced. Returning to your world.
{30, 141080, u"The host started the battle against\n<?npcNameId@212030?>."}, // Battle with <?npcNameId@212030?> has commenced. Returning to your world.
{30, 141090, u"The host started the battle against\n<?npcNameId@216000?>."}, // Battle with <?npcNameId@216000?> has commenced. Returning to your world.
{30, 141100, u"The host started the battle against\n<?npcNameId@232000?>."}, // Battle with <?npcNameId@232000?> has commenced. Returning to your world.
{30, 141110, u"The host started the battle against\n<?npcNameId@251000?>."}, // Battle with <?npcNameId@251000?> has commenced. Returning to your world.
{30, 141120, u"The host started the battle against\n<?npcNameId@257000?>."}, // Battle with <?npcNameId@257000?> has commenced. Returning to your world.
{30, 141130, u"The host started the battle against\n<?npcNameId@271000?>."}, // Battle with <?npcNameId@271000?> has commenced. Returning to your world.
{30, 141140, u"The host started the battle against\n<?npcNameId@1000?>."}, // Battle with <?npcNameId@1000?> has commenced. Returning to your world.
{30, 141150, u"The host started the battle against\n<?npcNameId@272000?>."}, // Battle with <?npcNameId@272000?> has commenced. Returning to your world.
{30, 141160, u"The host started the battle against\n<?npcNameId@305000?>."}, // Battle with <?npcNameId@305000?> has commenced. Returning to your world.
{30, 141170, u"The host started the battle against\n<?npcNameId@306000?>."}, // Battle with <?npcNameId@306000?> has commenced. Returning to your world.
{30, 141180, u"The host started the battle against\n<?npcNameId@313000?>."}, // Battle with <?npcNameId@313000?> has commenced. Returning to your world.
{30, 141190, u"The host started the battle against\n<?npcNameId@500000?>."}, // Battle with <?npcNameId@500000?> has commenced. Returning to your world.
{30, 141200, u"The host started the battle against\n<?npcNameId@501000?>."}, // Battle with <?npcNameId@501000?> has commenced. Returning to your world.
{30, 141210, u"The host started the battle against\n<?npcNameId@502000?>."}, // Battle with <?npcNameId@502000?> has commenced. Returning to your world.
{30, 141220, u"The host started the battle against\n<?npcNameId@503000?>."}, // Battle with <?npcNameId@503000?> has commenced. Returning to your world.
{30, 141230, u"The host started the battle against\n<?npcNameId@504000?>."}, // Battle with <?npcNameId@504000?> has commenced. Returning to your world.
{30, 141240, u"The host started the battle against\n<?npcNameId@505000?>."}, // Battle with <?npcNameId@505000?> has commenced. Returning to your world.
{30, 141250, u"The host started the battle against\n<?npcNameId@506000?>."}, // Battle with <?npcNameId@506000?> has commenced. Returning to your world.
{30, 141260, u"The host started the battle against\n<?npcNameId@507000?>."}, // Battle with <?npcNameId@507000?> has commenced. Returning to your world.
{30, 141270, u"The host started the battle against\n<?npcNameId@508000?>."}, // Battle with <?npcNameId@508000?> has commenced. Returning to your world.
{30, 141280, u"The host started the battle against\n<?npcNameId@509000?>."}, // Battle with <?npcNameId@509000?> has commenced. Returning to your world.
{30, 141290, u"The host started the battle against\n<?npcNameId@510000?>."}, // Battle with <?npcNameId@510000?> has commenced. Returning to your world.
{30, 141300, u"The host started the battle against\n<?npcNameId@511000?>."}, // Battle with <?npcNameId@511000?> has commenced. Returning to your world.
{30, 141310, u"The host started the battle against\n<?npcNameId@512000?>."}, // Battle with <?npcNameId@512000?> has commenced. Returning to your world.
{30, 141320, u"The host started the battle against\n<?npcNameId@513000?>."}, // Battle with <?npcNameId@513000?> has commenced. Returning to your world.
{30, 141330, u"The host started the battle against\n<?npcNameId@540000?>."}, // Battle with <?npcNameId@540000?> has commenced. Returning to your world.
{30, 141340, u"The host started the battle against\n<?npcNameId@551000?>."}, // Battle with <?npcNameId@551000?> has commenced. Returning to your world.
{30, 141350, u"The host started the battle against\n<?npcNameId@750000?>."}, // Battle with <?npcNameId@750000?> has commenced. Returning to your world.
{30, 141360, u"The host started the battle against\n<?npcNameId@804000?>."}, // Battle with <?npcNameId@804000?> has commenced. Returning to your world.
{30, 141361, u"The host started the battle against\n<?npcNameId@805000?>."}, // Battle with <?npcNameId@805000?> has commenced. Returning to your world.
{30, 141380, u"The host started the battle against\n<?npcNameId@899000?>."}, // Battle with <?npcNameId@899000?> has commenced. Returning to your world.
{30, 105100, u"The host started the battle against\n<?npcNameId@500000?>."}, // Beckoner has challenged <?npcNameId@500000?>.
{30, 105199, u"The host started a boss battle."}, // The beckoner has begun the final battle.
{30, 105299, u"The host started a boss battle."}, // The beckoner has begun the final battle. Returning to own world.
{30, 105399, u"The host started a boss battle."}, // The Executioner has begun the final battle. Returning to own world.
{30, 105499, u"The host started a boss battle."}, // The Vileblood has begun the final battle. Returning to own world.
{30, 105599, u"The host started a boss battle."}, // The blood-addled hunter has begun the final battle. Returning to own world.
{30, 106100, u"<?npcNameId@1000?> defeated."}, // <?npcNameId@1000?> was defeated, ending the hunt. Now, say farewell.
{30, 142010, u"<?npcNameId@1010?> defeated."}, // <?npcNameId@1010?> was defeated, ending the hunt. Now, say farewell.
{30, 142020, u"<?npcNameId@106000?> defeated."}, // <?npcNameId@106000?> was defeated, ending the hunt. Now, say farewell.
{30, 142030, u"<?npcNameId@127000?> defeated."}, // <?npcNameId@127000?> was defeated, ending the hunt. Now, say farewell.
{30, 142040, u"<?npcNameId@209000?> defeated."}, // <?npcNameId@209000?> was defeated, ending the hunt. Now, say farewell.
{30, 142050, u"<?npcNameId@210000?> defeated."}, // <?npcNameId@210000?> was defeated, ending the hunt. Now, say farewell.
{30, 142060, u"<?npcNameId@212010?> defeated."}, // <?npcNameId@212010?> was defeated, ending the hunt. Now, say farewell.
{30, 142070, u"<?npcNameId@212020?> defeated."}, // <?npcNameId@212020?> was defeated, ending the hunt. Now, say farewell.
{30, 142080, u"<?npcNameId@212030?> defeated."}, // <?npcNameId@212030?> was defeated, ending the hunt. Now, say farewell.
{30, 142090, u"<?npcNameId@216000?> defeated."}, // <?npcNameId@216000?> was defeated, ending the hunt. Now, say farewell.
{30, 142100, u"<?npcNameId@232000?> defeated."}, // <?npcNameId@232000?> was defeated, ending the hunt. Now, say farewell.
{30, 142110, u"<?npcNameId@251000?> defeated."}, // <?npcNameId@251000?> was defeated, ending the hunt. Now, say farewell.
{30, 142120, u"<?npcNameId@257000?> defeated."}, // <?npcNameId@257000?> was defeated, ending the hunt. Now, say farewell.
{30, 142130, u"<?npcNameId@271000?> defeated."}, // <?npcNameId@271000?> was defeated, ending the hunt. Now, say farewell.
{30, 142140, u"<?npcNameId@272000?> defeated."}, // <?npcNameId@272000?> was defeated, ending the hunt. Now, say farewell.
{30, 142150, u"<?npcNameId@304000?> defeated."}, // <?npcNameId@304000?> was defeated, ending the hunt. Now, say farewell.
{30, 142160, u"<?npcNameId@305000?> defeated."}, // <?npcNameId@305000?> was defeated, ending the hunt. Now, say farewell.
{30, 142170, u"<?npcNameId@306000?> defeated."}, // <?npcNameId@306000?> was defeated, ending the hunt. Now, say farewell.
{30, 142180, u"<?npcNameId@313000?> defeated."}, // <?npcNameId@313000?> was defeated, ending the hunt. Now, say farewell.
{30, 142190, u"<?npcNameId@500000?> defeated."}, // <?npcNameId@500000?> was defeated, ending the hunt. Now, say farewell.
{30, 142200, u"<?npcNameId@501000?> defeated."}, // <?npcNameId@501000?> was defeated, ending the hunt. Now, say farewell.
{30, 142210, u"<?npcNameId@502000?> defeated."}, // <?npcNameId@502000?> was defeated, ending the hunt. Now, say farewell.
{30, 142220, u"<?npcNameId@503000?> defeated."}, // <?npcNameId@503000?> was defeated, ending the hunt. Now, say farewell.
{30, 142230, u"<?npcNameId@504000?> defeated."}, // <?npcNameId@504000?> was defeated, ending the hunt. Now, say farewell.
{30, 142240, u"<?npcNameId@505000?> defeated."}, // <?npcNameId@505000?> was defeated, ending the hunt. Now, say farewell.
{30, 142250, u"<?npcNameId@506000?> defeated."}, // <?npcNameId@506000?> was defeated, ending the hunt. Now, say farewell.
{30, 142260, u"<?npcNameId@507000?> defeated."}, // <?npcNameId@507000?> was defeated, ending the hunt. Now, say farewell.
{30, 142270, u"<?npcNameId@508000?> defeated."}, // <?npcNameId@508000?> was defeated, ending the hunt. Now, say farewell.
{30, 142280, u"<?npcNameId@509000?> defeated."}, // <?npcNameId@509000?> was defeated, ending the hunt. Now, say farewell.
{30, 142290, u"<?npcNameId@510000?> defeated."}, // <?npcNameId@510000?> was defeated, ending the hunt. Now, say farewell.
{30, 142300, u"<?npcNameId@511000?> defeated."}, // <?npcNameId@511000?> was defeated, ending the hunt. Now, say farewell.
{30, 142310, u"<?npcNameId@512000?> defeated."}, // <?npcNameId@512000?> was defeated, ending the hunt. Now, say farewell.
{30, 142320, u"<?npcNameId@513000?> defeated."}, // <?npcNameId@513000?> was defeated, ending the hunt. Now, say farewell.
{30, 142330, u"<?npcNameId@540000?> defeated."}, // <?npcNameId@540000?> was defeated, ending the hunt. Now, say farewell.
{30, 142340, u"<?npcNameId@551000?> defeated."}, // <?npcNameId@551000?> was defeated, ending the hunt. Now, say farewell.
{30, 142350, u"<?npcNameId@750000?> defeated."}, // <?npcNameId@750000?> was defeated, ending the hunt. Now, say farewell.
{30, 142360, u"<?npcNameId@804000?> defeated."}, // <?npcNameId@804000?> was defeated, ending the hunt. Now, say farewell.
{30, 142370, u"<?npcNameId@805000?> defeated."}, // <?npcNameId@805000?> was defeated, ending the hunt. Now, say farewell.
{30, 142380, u"<?npcNameId@899000?> defeated."}, // <?npcNameId@899000?> was defeated, ending the hunt. Now, say farewell.
{30, 106199, u"Boss defeated."}, // The final battle has ended. It is time to bid farewell.
{30, 102002, u"Invader <?joinName?> entered your world!"}, // Adversary <?joinName?> has arrived.
{30, 109000, u"A bell-ringing woman appeared.\nAn invader may come."}, // Drawn by the bell's timbre, a bell-ringing woman has appeared somewhere.
{30, 103002, u"Invader <?deadName?> was killed."}, // Adversary <?deadName?> has died.
{30, 103003, u"Invader <?deadName?> was killed."}, // Vileblood <?deadName?> has died.
{30, 103004, u"Invader <?deadName?> was killed."}, // Executioner <?deadName?> has died.
{30, 103005, u"Invader <?deadName?> was killed."}, // Hunter of Hunters, <?deadName?>, has died.
{30, 106300, u"The host <?hostName?> died.\nYou stay in their game."}, // Executioner <?hostName?> has fallen. It is time to bid farewell.
{30, 106400, u"The host <?hostName?> died.\nYou stay in their game."}, // Vileblood <?hostName?> has fallen. It is time to bid farewell.
{30, 106500, u"The host <?hostName?> died.\nYou stay in their game."}, // Blood-addled hunter <?hostName?> has fallen. It is time to bid farewell.
{30, 20000407, u"Invader <?leaveName?> left\nand went back to their world."}, // Adversary <?leaveName?> returned to their own world.
{30, 20000408, u"Invader <?leaveName?> left\nand went back to their world."}, // Adversary <?leaveName?> returned to their own world.
{30, 20000409, u"Invader <?leaveName?> left\nand went back to their world."}, // Vileblood <?leaveName?> returned to their own world.
{30, 20000410, u"Invader <?leaveName?> left\nand went back to their world."}, // Vileblood <?leaveName?> returned to their own world.
{30, 20000411, u"Invader <?leaveName?> left\nand went back to their world."}, // Executioner <?leaveName?> returned to their own world.
{30, 20000412, u"Invader <?leaveName?> left\nand went back to their world."}, // Executioner <?leaveName?> returned to their own world.
{30, 20000413, u"Invader <?leaveName?> left\nand went back to their world."}, // Hunter of Hunters <?leaveName?> returned to their own world.
{30, 20000414, u"Invader <?leaveName?> left\nand went back to their world."}, // Hunter of Hunters <?leaveName?> returned to their own world.
{30, 102300, u"You invaded <?hostName?>'s world."}, // You are now in the world of the beckoner, Executioner <?hostName?>
{30, 102400, u"You invaded <?hostName?>'s world."}, // You are now in the world of the beckoner, Vileblood <?hostName?>
{30, 102500, u"You invaded <?hostName?>'s world."}, // You are now in the world of the beckoner, the blood-addled hunter <?hostName?>.
{30, 101001, u"A player answered your bell.\nJoining..."}, // Discovered the chime of a Small Resonant Bell.
{30, 101003, u"A player answered your bell.\nJoining..."}, // Discovered the chime of a Small Resonant Bell.
{30, 101004, u"A player answered your bell.\nJoining..."}, // Discovered the chime of a Small Resonant Bell.
{30, 101005, u"A player answered your bell.\nJoining..."}, // Discovered the chime of a Small Resonant Bell.
{30, 101100, u"Found a host.\nJoining their game..."}, // Discovered the chime of a Beckoning Bell.
{30, 14000, u"Ring the Hunt Bell\nto call other players?"}, // Expend Insight and seek the assistance of a hunter from another world?
{30, 14003, u"Stop the Hunt Bell"}, // Silence the Beckoning Bell
{30, 14004, u"Stop the Kin Bell"}, // Silence the Small Resonant Bell
{30, 14005, u"Stop the Bloodlust Bell"}, // Silence the Sinister Resonant Bell
{30, 100001, u"Hunt Bell rung.\nWaiting for players to join..."}, // Searching for the chime of a Small Resonant Bell...
{30, 100100, u"Kin Bell rung.\nLooking for a host to join..."}, // Searching for the chime of a Beckoning Bell...
{30, 100200, u"Bloodlust Bell rung.\nLooking for a world to invade..."}, // Searching for the chime of a Sinister Bell...
{30, 100300, u"Nobody found.\nRing the bell again to keep looking."}, // Could not discover the chime of a Small Resonant Bell. No insight was consumed.
{30, 101002, u"Someone is about to invade your world."}, // Sinister Bell resonates with another...
{30, 101200, u"Found a world to invade.\nEntering..."}, // Discovered the chime of a Sinister Bell.
{30, 100011, u"Hunt Bell rung (password).\nWaiting for players to join..."}, // Searching for the chime of a Small Resonant Bell with matching password.
{30, 100110, u"Kin Bell rung (password).\nLooking for a host to join..."}, // Searching for the chime of a Beckoning Bell with matching password.
{30, 101300, u"Found a host,\nbut the beckoner is an Executioner, and thus an adversary."}, // Discovered the chime of a Beckoning Bell, but the beckoner is an Executioner, and thus an adversary.
{30, 101400, u"Found a host,\nbut the beckoner is a Vileblood, and thus an adversary."}, // Discovered the chime of a Beckoning Bell, but the beckoner is a Vileblood, and thus an adversary.
{30, 101500, u"Found a host,\nbut the beckoner is a blood-addled hunter, and thus an adversary."}, // Discovered the chime of a Beckoning Bell, but the beckoner is a blood-addled hunter, and thus an adversary.
{30, 10011270, u"Ring the Hunt Bell to call\nother players into your game."}, // Consume Insight and ring the Beckoning Bell to enlist the cooperation of hunters from other worlds.
{30, 10011280, u"Use the Parting Blank to end co-op."}, // Silencing Blank ends such forms of cooperation.
{200, 266302, u"Bloodlust Bell"}, // Sinister Bell
{200, 280140, u"Bloodlust Bell"}, // Sinister Bell
{200, 280240, u"Bloodlust Bell"}, // Sinister Bell
{200, 302000, u"Parting Blank"}, // Silencing Blank
{200, 302100, u"Parting Blank"}, // Silencing Blank
{200, 302110, u"Stop the Hunt Bell"}, // Silence Beckoning Bell
{200, 302111, u"Stop the Kin Bell"}, // Silence Small Resonant Bell
{200, 302112, u"Stop the Bloodlust Bell"}, // Silence Sinister Resonant Bell
{200, 302120, u"End co-op"}, // Disrupt Resonance
{200, 302123, u"End co-op"}, // Disrupt Resonance
{200, 302124, u"End co-op and go home"}, // Disrupt Resonance and Return Home
{201, 269002, u"Quick search: find a host and join their game."}, // Quick-search a Beckoning Bell chime to begin co-op.
{201, 269003, u"Quick search: find a world to invade."}, // Quick-search a Sinister Bell chime to become an adversary.
{201, 302110, u"Stop calling other players."}, // Silence Bell and cease requesting co-op.
{201, 302111, u"Stop looking for a host."}, // Silence Bell and stop helping other Hunters.
{201, 302112, u"Stop looking for a world to invade."}, // Silence Bell and cease threat.
{204, 269001, u"Find a host and join their game?"}, // Quick-search a Beckoning Bell chime and begin co-op?
{204, 269002, u"Find a world to invade?"}, // Quick-search a Sinister Bell chime and become an adversary?
{204, 302200, u"Kin Bell rung.\nLooking for a host to join..."}, // Searching for the chime of a Beckoning Bell...
{204, 302201, u"Bloodlust Bell rung.\nLooking for a world to invade..."}, // Searching for the chime of a Sinister Bell...
{204, 302210, u"Kin Bell rung (password).\nLooking for a host to join..."}, // Searching for the chime of a Beckoning Bell with matching password.
{2, 2000, u"Need help? Ring the Hunt Bell."}, // Ring the Beckoning Bell if you require assistance.
{2, 1121, u"Call other players into your game\nwith the Hunt Bell."}, // Receive assistance from the hunters of other worlds by using the Beckoning Bell.
{2, 1122, u"Join another player's game\nwith the Kin Bell."}, // Assist the hunters of other worlds by using the Small Resonant Bell.
{30, 15000011, u"You caught up with <?hostName?>."}, // *
{30, 15000012, u"<?joinName?> caught up with you."}, // *
{30, 15000035, u"You are in the host's Hunter's Dream."}, // *
{10, 4016, u"Hunt Bell"}, // (empty)
{20, 4016, u"Ring to call other players into your game.\nCosts no Insight."}, // (empty)
{24, 4016, u"An old bell carried by the hunters of the\nworkshop, its toll heard across every world.\n\nRing it, and hunters who answer with a\nKin Bell may join your hunt. Together you\nwalk the night, through every lamp,\ndeath and dream, until the hunt is done."}, // (empty)
{10, 4121, u"Kin Bell"}, // (empty)
{20, 4121, u"Ring to join another players game.\nCosts no Insight."}, // (empty)
{24, 4121, u"A small bell of the choir, its chime\nfaint, but true.\n\nHunters who hear a Hunt Bell may answer\nwith it, crossing into another world to\nstand at the side of kin. What is slain\nthere is slain for both."}, // (empty)
{10, 11201, u"Bloodlust Bell"}, // (empty)
{20, 11201, u"Ring to invade another players world.\nCosts no Insight."}, // (empty)
{24, 11201, u"A bell that sounds only for the blood-drunk.\n\nNo maiden need ring for its toll to be\nheard: every hunter of the night is prey,\nalone or in company, wherever they walk.\n\nThe hunt for hunters never ends."}, // (empty)
{10, 11251, u"Parting Blank"}, // (empty)
{20, 11251, u"Use to end co-op.\nGuests go home; the host sends everyone home."}, // (empty)
{24, 11251, u"A blank of silent powder.\n\nIts inaudible burst breaks the resonance\nbinding hunters of different worlds, and\neach returns to their own night."}, // (empty)
{20, 200, u"No longer used. Use the Hunt Bell."}, // Use Insight to ring and call to other worlds for help.
{20, 205, u"No longer used. Use the Kin Bell."}, // Ring this bell to help hunters in other worlds.
{20, 225, u"No longer used. Use the Bloodlust Bell."}, // Ring to become the enemy of a hunter in another world.
{20, 111, u"No longer used. Use the Parting Blank."}, // Fire to end ongoing, and prevent further, cooperation.
{10, 4313, u"Duel Bell"}, // (empty)
{20, 4313, u"Ring in co-op to turn on PvP between players.\nRing again to turn it off."}, // (empty)
{24, 4313, u"A cracked bell with a sour, discordant chime.\n\nRing it while hunting together, and the\nresonance binding the hunters turns to\nrivalry: they may strike one another as foes.\n\nRing it again to restore the old bond.\nOnly the beckoner decides; a cooperator\nwho rings it merely asks."}, // (empty)
+493
View File
@@ -0,0 +1,493 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
// BBSC byte patches for Bloodborne CUSA03173 01.09: the sites, their expected bytes and what they
// become. Shared by the emulator build (bbsc_persistence.cpp) and the injected mod DLL
// (mod/bbsc_mod.cpp), so both apply exactly the same list. Standard library only.
#pragma once
#include <array>
#include <cstdint>
#include <cstring>
#include "core/bbsc_persistence.h"
namespace Core::Bbsc::PatchTables {
using u8 = std::uint8_t;
using u64 = std::uint64_t;
// CUSA03173 01.09 addresses (eboot sha256 d65f0b4f...). See docs/SESSION-LIFECYCLE.md.
constexpr u64 MultiPlayStopOffset = 0x01ED07A0; // CSMultiPlayMan::Stop
constexpr u64 MatchingExistsOffset = 0x01ED00A0; // CSMultiPlayMan "matching controller exists"
constexpr u64 RemoteCleanupOffset = 0x015BED10; // WorldChrMan remote-player cleanup
constexpr u64 ClearInsertTasksOffset = 0x01E54830; // CSMultiPlayMan: destroy every member task
constexpr u64 RemoveMemberOffset = 0x01E54DB0; // CSMultiPlayMan::RemoveMember(id)
constexpr u64 RemoveMemberTwinOffset = 0x01E55050; // second member list removal
constexpr u64 MarkSlotsLeavingOffset = 0x014EB860; // marks every remote slot leaving
constexpr u64 DeathReloadRequestOffset = 0x013CDE30; // StageTransition request at the end of death
enum class Group : u8 {
Stage, // map loads: StageTransition, stage helper, stage-UID policy, stage flow, destructor
Revive, // OnReviveMagic_1 reload
Boss, // BlockClearAnim / boss-area leave
MatchCheck, // OnMatchingCheck (missing remote character) — weakens disconnect detection
Lamp, // lamp prompt 4th availability byte (+0x4B) from 0x0191B6E0
Tasks, // keep the per-member CSMultiPlayerIns tasks across a kept load (host death)
HostDead, // guest ignores the host's death instead of being sent home through a load
NoFogWalls, // co-op area fog walls never appear
LeaveLimit, // leaving the co-op area does not send a guest home
Spectate, // the death steps run in full except for the reload they request at the end
Echoes, // a summoned guest keeps the whole blood echo reward, not half of it
GuestItems, // a summoned guest sees, picks up and keeps the items of the host's world
SolidNpcs, // a guest sees the host world's NPCs as they are, not as grey wandering ghosts
Scaling, // vanilla's boss co-op buff (Lua_MultiDoping) off - BBSC enemy scaling instead
KitBells, // the BBSC kit's Hunt Bell gets the Beckoning Bell's special checks (Dream use)
SendBacklog, // one member that never answers does not end the session for everyone
Hunters, // the host keeps looking for co-op signs past vanilla's two guests
};
// Lamp action update 0x012F5C40 hides the lamp prompt unless bytes +0x48..+0x4B are all zero.
// +0x49 (multiplayer) is cleared for the host by the shadp2p seamless hook; +0x4B is written
// later from `call 0x0191B6E0` (WorldChrMan scan of characters bound to the lamp entity), which
// the shadp2p hook runs too early to affect. Retargeting that single call to a native
// `xor eax,eax; ret` clears the fourth gate at this call site only.
constexpr u64 LampCharacterGateOffset = 0x0191B6E0;
constexpr u64 ReturnZeroOffset = 0x01300AA0;
struct CallRetarget {
const char* name;
u64 site; // offset of the 5-byte call/jmp rel32
u8 opcode; // 0xE8 call or 0xE9 jmp
u64 original; // expected current target
u64 target; // replacement target
Group group;
};
// Every Stop site below is followed by `test al,al; je` and stores a session field only on
// success; MatchingExists returns true exactly when a room is active, so callers keep their
// native success branch without the session being torn down. The paired cleanup call is
// redirected to a return-only function.
constexpr std::array<CallRetarget, 27> Retargets{{
// SoloPlayDeath_2 (0x01381DE0) and PartyGhostDeath_2 (0x013824C0) are the last step of the
// death script. Both end with `0x01332BC0()` (pick the reload destination) and
// `0x013CDE30()` (request the StageTransition that rebuilds the world). BBSC used to skip the
// whole step, which left the step's own bookkeeping undone: the death UI objects at
// global+0x1690/+0x1698/+0x16B0/+0x16B8 were never released, so a SECOND death never reached
// the step again and the player stayed dead with only camera control (run 2026-09-19, host
// log: death armed, no "death held", room timed out 60 s later). Dropping only the reload
// call lets the step finish - including its own blood echo drop - while the world stays up.
{"SoloPlayDeath_2.Reload", 0x01381EDE, 0xE8, DeathReloadRequestOffset, PersistNoOpOffset,
Group::Spectate},
{"PartyGhostDeath_2.Reload", 0x01382663, 0xE8, DeathReloadRequestOffset, PersistNoOpOffset,
Group::Spectate},
{"Lamp.CharacterGate", 0x012F83BC, 0xE8, LampCharacterGateOffset, ReturnZeroOffset, Group::Lamp},
{"StageTransition.Stop", 0x013CDEF6, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
{"StageTransition.Cleanup", 0x013CDF17, 0xE8, RemoteCleanupOffset, PersistNoOpOffset, Group::Stage},
{"StageHelper.Stop", 0x013CDDF5, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
{"StageHelper.Cleanup", 0x013CDE26, 0xE9, RemoteCleanupOffset, PersistNoOpOffset, Group::Stage},
{"StageUidPolicy.Stop", 0x0193C657, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
{"StageUidPolicy.Cleanup", 0x0193C689, 0xE8, RemoteCleanupOffset, PersistNoOpOffset, Group::Stage},
{"StageFlow.Stop", 0x01939876, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
// Also retargeted by the shadp2p seamless patches; accepted in either state.
{"StageTaskDestructor.Stop", 0x019471B1, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Stage},
{"Revive.Stop", 0x01389DDD, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Revive},
{"Revive.Cleanup", 0x01389DFE, 0xE8, RemoteCleanupOffset, PersistNoOpOffset, Group::Revive},
{"BossArea.Stop", 0x0138B410, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::Boss},
{"BossArea.Cleanup", 0x0138B437, 0xE8, RemoteCleanupOffset, PersistNoOpOffset, Group::Boss},
{"MatchingCheck.Stop", 0x013809B7, 0xE8, MultiPlayStopOffset, MatchingExistsOffset, Group::MatchCheck},
{"MatchingCheck.Cleanup", 0x013809E8, 0xE9, RemoteCleanupOffset, PersistNoOpOffset, Group::MatchCheck},
// Stage task destructor (0x019470F0) destroys every member task when the summon reload phase
// is not 3. A host death is exactly that case, so the host loses the task that recreates the
// guest's character and can no longer see the guest (observed 2026-09-16: host shape ins=[]).
{"StageTaskDestructor.ClearIns", 0x01947207, 0xE8, ClearInsertTasksOffset, PersistNoOpOffset,
Group::Tasks},
// PlayerIns death handler 0x018C49F0: when the LOCAL player dies and the session state is 3
// (host), it calls RemoveMember for every member task, the twin removal for the second list,
// and marks every remote slot as leaving. That is the host-death kick (trace 2026-09-16:
// RemoveMember x2 from 0x018C4D82 right at the host's death). Only the host branch is patched.
{"HostOnDeath.RemoveMember", 0x018C4D7D, 0xE8, RemoveMemberOffset, PersistNoOpOffset,
Group::HostDead},
{"HostOnDeath.RemoveMemberTwin", 0x018C4DAC, 0xE8, RemoveMemberTwinOffset, PersistNoOpOffset,
Group::HostDead},
{"HostOnDeath.MarkSlotsLeaving", 0x018C4DBA, 0xE8, MarkSlotsLeavingOffset, PersistNoOpOffset,
Group::HostDead},
// Guest item pickup, part 6 (2026-10-04, "items cannot be picked up"). Guest log 2026-10-02:
// the map's items were there right after every load (list +0x28: 132 items, 119 pickable) and
// the whole list was gone 2-3 s after each "reinsert v8: restarted ... FirstSyncWait". The item
// manager's network update 0x017D2F30 reads packet 0x2C; one message kind is "drop your items"
// with a list selector ([rbp-0x19C] + 1, jump table 0x017D4C2C): -1 = all three lists
// (0x017D3498/34A0/34A8), or one list (0x017D4089/409A/40AB). 0x017D59F0 / 5B40 / 5C40 free the
// list's items with +0x48 bit 0x80 and then zero the list head, so every map item vanished
// while its glow could stay. In vanilla the guest never owns map items, so the host's join sync
// clears them; with BBSC the guest makes its own (GuestItems.SpawnOwn), so the clear is dropped.
// Nothing else calls these three.
{"GuestItems.KeepOnSyncAll1", 0x017D3498, 0xE8, 0x017D59F0, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSyncAll2", 0x017D34A0, 0xE8, 0x017D5B40, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSyncAll3", 0x017D34A8, 0xE8, 0x017D5C40, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSync1", 0x017D4089, 0xE8, 0x017D59F0, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSync3", 0x017D409A, 0xE8, 0x017D5C40, ReturnZeroOffset, Group::GuestItems},
{"GuestItems.KeepOnSync2", 0x017D40AB, 0xE8, 0x017D5B40, ReturnZeroOffset, Group::GuestItems},
}};
// Functions made to return immediately. Only for Lua-registered C functions whose whole job is a
// session-ending action, so skipping all of it leaves nothing half-applied.
struct EntryReturn {
const char* name;
u64 site;
std::array<u8, 6> prologue; // expected first bytes
Group group;
};
constexpr std::array<EntryReturn, 3> EntryReturns{{
// OnLeave_Limit (Lua, 0x0138A4A0): a living guest who leaves the co-op area is sent home
// (event state 2, forced warp global+0x1520, home respawn point 0x01332BC0(-1), StageTransition).
// With the fog walls gone the guest may walk past their old spot, so the step is skipped.
// Skipping it also removes the only way a guest gets sent home, so it is off by default:
// with it on the players reported not being able to leave the world (run 2026-09-19).
{"OnLeave_Limit", 0x0138A4A0, {0x55, 0x48, 0x89, 0xE5, 0x53, 0x50}, Group::LeaveLimit},
// HostDead (Lua, 0x01381870): the first step on the guest when the host dies (returns at once
// on the host itself, state 3). It sets event flag 4047 (0x0132E5A0), shows the host-dead
// message and sets death-pending [0x0553B130]+0x70 (0x01386960), and schedules HostDead_1 and
// EventMenuBrake (menu brake MenuMan+0x2DC). With the guest staying in the world none of that
// is wanted; after it ran the guest could not use ladders (run 2026-09-16).
{"HostDead", 0x01381870, {0x55, 0x48, 0x89, 0xE5, 0x41, 0x57}, Group::HostDead},
// HostDead_1 (Lua, registered at 0x0138DADB): runs on the guest when the host dies. It sets the
// forced-warp byte (global+0x1520), selects the home respawn point (0x01332BC0(-1)), starts a
// StageTransition (0x013CDE30) and queues the original player type (+0x8C). Skipping it keeps
// the guest standing where he is, with no loading screen, while the host respawns.
{"HostDead_1", 0x01381A60, {0x55, 0x48, 0x89, 0xE5, 0x53, 0x50}, Group::HostDead},
}};
// Two-byte instruction rewrites, verified against the bytes that must be there.
struct ByteEdit {
const char* name;
u64 site;
std::array<u8, 2> original;
std::array<u8, 2> replacement;
Group group;
};
constexpr std::array<ByteEdit, 17> ByteEdits{{
// The lamp prompt in 0x012F5C40 needs all four of +0x48, +0x49, +0x4A and +0x4B to be zero.
// +0x49 is the "I am in a multiplayer session" flag, built right here:
// CMP dword ptr [RBX + 0x14], 0x1
// SETG AL ; > 1 members
// TEST ECX, ECX
// SETG CL ; SprjSessionManager member vector is non-empty
// OR CL, AL <- 0x012F8321, the two bytes we replace
// MOV byte ptr [R13 + 0x49], CL
// "OR CL,AL" (08 C1) becomes "XOR CL,CL" (30 C9), so the flag stores 0 and the prompt appears
// in co-op. Same length, same register, nothing after it moves.
//
// This replaces what Lamp.CharacterGate was meant to do. That entry neutralises 0x0191B6E0,
// but the decompile shows that call only runs when +0x49 is already zero - so in a session it
// never executed and the patch did nothing. Reported 2026-09-22: no lamp prompt at all, with
// lamp=true in the log.
{"Lamp.MultiplayerGate", 0x012F8321, {0x08, 0xC1}, {0x30, 0xC9}, Group::Lamp},
// The "Travel" prompt itself comes from the lamp's talk script, not from 0x012F5C40. The talk
// condition dispatcher 0x0203AD60 answers the script function "is multiplayer in progress"
// (case 0x34) with the same expression as +0x49 above:
// CMP dword ptr [RBX + 0x14], 0x1 / SETG AL / TEST ECX, ECX / SETG CL
// OR CL, AL <- 0x0203C564
// MOVZX EAX, CL ; the script's answer
// Clearing it makes every talk script on the HOST see a solo game (lamp prompt, and anything
// else the scripts gate on co-op). The guest keeps the vanilla answer: Group::Lamp is a role
// group that is only switched on while hosting.
{"Lamp.TalkMultiplayerCheck", 0x0203C564, {0x08, 0xC1}, {0x30, 0xC9}, Group::Lamp},
// Guest item pickup, part 2 (part 1 is below in WideEdits). Picking up a map treasure is
// 0x017D8570(item manager [0x0553D6E0], &item id). For a treasure (item +0x48 bit 0x80) it asks
// the session state first:
// CMP dword ptr [RAX + 0x124], 0x6 ; SprjSessionManager: summoned guest
// JNZ 0x017D8768 <- 0x017D85FB: not a guest -> take it locally
// ... ; a guest only sends packet 0x2C to the host
// and later, on that local path:
// CMP dword ptr [RAX + 0x124], 0x6
// JZ 0x017D88D3 <- 0x017D88C2: a guest skips 0x017D89F0 (give items)
// So a guest takes the host's path: the item is marked taken in the guest's game and its lot is
// given to the guest. The inventory is saved live (the guest's save writer 0x0156AA50 only swaps
// world snapshots), so the guest keeps it; the host's world is not touched and the host can pick
// the same item up too. 0F 85 -> 90 E9 keeps the displacement and the target (0x017D8768).
{"GuestItems.PickupAsHost", 0x017D85FB, {0x0F, 0x85}, {0x90, 0xE9}, Group::GuestItems},
{"GuestItems.GiveToGuest", 0x017D88C2, {0x74, 0x0F}, {0x90, 0x90}, Group::GuestItems},
// Guest item pickup, part 3 (2026-09-26, "guest can't see items", NG+ guest that took nothing):
// the guest's map items were never created. Spawning an item (0x017D8370, called for the map's
// treasures and for drops) does, for a summoned guest,
// CMP dword ptr [RAX + 0x124], 0x6
// JNZ 0x017D8511 <- 0x017D83D2: host/solo -> create it here (0x017DE870)
// and otherwise only sends packet 0x2C type 3 to the host - nothing ever comes back as an item
// the guest can see. 0F 85 -> 90 E9: the guest creates its own, from its own map load and its
// own pickup flags, so each player sees the items they have not taken (personal loot).
{"GuestItems.SpawnOwn", 0x017D83D2, {0x0F, 0x85}, {0x90, 0xE9}, Group::GuestItems},
// Part 4: marking a picked-up item taken (its pickup flag +0x28 and count flag +0xB8, in
// 0x017D6780) is skipped for a guest outside its own world:
// CMP dword ptr [RAX + 0x124], 0x6 / MOV R14, RBX
// JZ 0x017D6AE8 <- 0x017D6ADC: guest -> "own world?" -> skip
// CMP dword ptr [R15 + 0x98], -1 / JZ 0x017D6AFC (set the flags)
// Without the flag a guest's item came back after every load. 74 0A -> EB 1E jumps straight
// to 0x017D6AFC. The flag lives in the live table of the host's world only; the guest's save
// keeps its own world (see bloodborne_re.cpp, world state).
{"GuestItems.MarkTaken", 0x017D6ADC, {0x74, 0x0A}, {0xEB, 0x1E}, Group::GuestItems},
// Part 5 (2026-09-29, "items cannot be seen by the guest", still on 5B726D9D): a guest's log
// showed 0x017D8370 and 0x017DE870 never run at all, host or guest, so parts 3 and 3b patched
// functions map treasures do not go through. Map treasures are placed by 0x017DC300(item man,
// entity, MSB event, ...), called per MSB event of type 4 (treasure) at map load, which for a
// summoned guest or anyone in another world skips the whole thing (see WideEdits, part 5).
// It then hands each treasure to 0x017D4C40 / 0x017D4FB0 / 0x017D5320 by lot kind, and each
// of those numbers the new item only when hosting or solo:
// CMP dword ptr [RAX + 0x124], 0x6 ; JZ skip <- guest
// CMP dword ptr [R15 + 0x98], -1 ; JNZ skip <- in another world
// MOV EAX, [RBX + 8] / INC / store: item number = counter << 4
// An item left unnumbered cannot be looked up by its pickup, so both jumps become NOPs: the
// guest numbers its own items from its own counter, as its game does in its own world.
{"GuestItems.NumberKind1", 0x017D4F62, {0x74, 0x26}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind1Away", 0x017D4F6F, {0x75, 0x1C}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind3", 0x017D52D9, {0x74, 0x26}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind3Away", 0x017D52E6, {0x75, 0x1C}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind2", 0x017D5649, {0x74, 0x26}, {0x90, 0x90}, Group::GuestItems},
{"GuestItems.NumberKind2Away", 0x017D5656, {0x75, 0x1C}, {0x90, 0x90}, Group::GuestItems},
// Grey NPCs (2026-09-29, guest, outside the Dream): the NPC constructor 0x018E4870 gives an
// NpcParam row with isChangeWanderGhost (+0x145 bit 7) its ghost model (+0x10C) and type 4
// whenever the local player is in someone else's world:
// CMP dword ptr [RAX + 0x124], 0x6 ; JZ ... / CMP [RBX + 0x98], -1 ; JZ 0x018E4FDC
// MOV ECX, [RAX + 0x145] / TEST CL, 0x80
// JZ 0x018E4FDC <- 0x018E4FBF: not a ghost NPC -> keep its own look
// ... ghost model, R12D = 4
// 74 1B -> EB 1B: every NPC keeps its own model and type for a guest, as the host sees it.
// Host and solo never reach this test. The Dream-only row edit (Doll, Gehrman) stays as well.
{"SolidNpcs.NoWanderGhost", 0x018E4FBF, {0x74, 0x1B}, {0xEB, 0x1B}, Group::SolidNpcs},
// Lua_MultiDoping (0x0138BC70): CMP EAX,2 / JNZ (2 guests -> SpEffect 7501) and CMP EAX,1 / JNZ
// (1 guest -> 7500). Both JNZ -> JMP: the boss never gets the vanilla buff, the game still
// sets its event flag 4070 after it. BBSC's own scaling (bloodborne_re.cpp) replaces it.
{"Scaling.NoVanillaTwoGuests", 0x0138BCC2, {0x75, 0x32}, {0xEB, 0x32}, Group::Scaling},
{"Scaling.NoVanillaOneGuest", 0x0138BCF9, {0x75, 0x54}, {0xEB, 0x54}, Group::Scaling},
// Boss insight for the guest (2026-10-08, testers: "guest only gets insight for discovering the
// boss, not for killing it"). The kill reward 0x018C5070, run in every player's game for every
// death, gives a boss's insight ("HumanityDrop", boss row via 0x01F1B4C0, then 0x01F1B760 /
// 0x01F1B830 / 0x028986B0) only behind
// CMP byte ptr [global + 0x1592], 0 ; in our own world?
// JZ 0x018C59A6 <- 0x018C52F9 (0F 84 A7 06 00 00): no -> nothing
// A guest is never in its own world. EB 04 jumps over the rest of the JZ to the next
// instruction (0x018C52FF), so the guest's copy gives the guest's insight too; host and solo
// never took the jump anyway.
{"Echoes.GuestBossInsight", 0x018C52F9, {0x0F, 0x84}, {0xEB, 0x04}, Group::Echoes},
// Five hunters (test 2026-10-08, host hardwater with 2 guests: a third player's co-op sign got
// 18 empty sign lists in a row, the same player's invader sign was found at once). The host's
// sign search 0x01872360 asks the server for co-op signs (type 7) only while one of vanilla's
// two co-op slots is free - the slot table 0x0186FE40 fills [rbp-0x9C] / [rbp-0x90] with the
// first and second guest:
// CMP [rbp-0x9C], 0 ; JZ 0x018723B1 ; first slot free -> search
// MOV EAX, [rbp-0x90] ; TEST EAX, EAX
// JNZ 0x018723FF <- 0x018723AF (75 4E): both taken -> no co-op search
// 75 4E -> 90 90: the co-op search goes on; the summon builder's own cap (4 cooperators,
// ApplyBbscHunterCap) still decides who is let in.
{"Hunters.KeepSearchingCoop", 0x018723AF, {0x75, 0x4E}, {0x90, 0x90}, Group::Hunters},
}};
// Four-byte rewrites (a RIP-relative displacement), verified against the bytes that must be there.
struct WideEdit {
const char* name;
u64 site;
std::array<u8, 4> original;
std::array<u8, 4> replacement;
Group group;
};
constexpr std::array<WideEdit, 11> WideEdits{{
// Every blood echo reward the local player receives is computed by 0x01335400 and left in a
// float at [[0x0553B130]+8]+0xF8. Halfway through:
// MOV EAX, dword ptr [R15 + 0x78] ; the local player's character type
// OR EAX, 0x8 / CMP EAX, 0x8
// JZ 0x01335596 ; host (type 0) and type 8 skip the next three
// VMOVSS XMM0, [RBP + -0x44]
// VMULSS XMM0, XMM0, [RIP + 0x035F058B] <- 0x01335589, disp32 at 0x0133558D
// VMOVSS [RBP + -0x44], XMM0 ; that address is 0x04925B1C = 0.5
// Skipping the multiply (JZ -> JMP, builds up to 39A08659) did not help: a guest still got
// exactly half the host's echoes for the same kill (+671 against +1341, both players NG+,
// 2026-09-24). That is because 0x01335400 is only the EVENT reward (registered as a Lua/event
// function at 0x013421AC); ordinary kills go through 0x018FC860, below. Doubling here
// (build 43B41805) changed nothing for kills either, and would have paid a guest 2x for event
// rewards, so the multiply now reads 1.0 - the float at 0x04925B20, right after the 0.5.
// New displacement: 0x04925B20 - 0x01335591 = 0x035F058F.
{"Echoes.GuestEventFull", 0x0133558D, {0x8B, 0x05, 0x5F, 0x03}, {0x8F, 0x05, 0x5F, 0x03},
Group::Echoes},
// The kill reward: 0x018FC860(rate, chr, enemy, bonus, guest_share), called by 0x018C5070
// with guest_share = [[0x05556678]+0x16F8]+0x98 != -1 || SprjSessionManager+0x124 == 6 (a
// summoned guest), multiplies by 0.5 when guest_share is set:
// 0x018FC947 VMOVSS XMM3, [RIP + 0x0302B571] ; 0x04927EC0 = 0.5
// and pays it through chr->vtable[0x3D0]. Pointing the load at 0x04927EBC (1.0, the float
// just before) pays a guest what the host gets: +671 against +1341 in every 2026-09-24 test.
// New displacement: 0x04927EBC - 0x018FC94F = 0x0302B56D.
{"Echoes.GuestKillFull", 0x018FC94B, {0x71, 0xB5, 0x02, 0x03}, {0x6D, 0xB5, 0x02, 0x03},
Group::Echoes},
// Guest item pickup, part 1. 0x017D9E70(item manager, &item id, on) makes an item pickable
// (+0x48 bit 0x40, which the pickup prompt search 0x017D9FB0 requires). For map treasures
// (bit 0x80) it refuses a summoned guest with the same two tests as the echo halving:
// CMP dword ptr [RCX + 0x124], 0x6 ; JZ return 0 (0F 84 B6000000 at 0x017D9EE7)
// CMP dword ptr [R15 + 0x98], -1 ; JNZ return 0 (0F 85 A8000000 at 0x017D9EF5)
// Zeroing both displacements makes each jump land on the next instruction, so the treasure is
// enabled for the guest exactly as for the host.
{"GuestItems.ShowTreasure", 0x017D9EE9, {0xB6, 0x00, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00},
Group::GuestItems},
{"GuestItems.ShowTreasureShare", 0x017D9EF7, {0xA8, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00}, Group::GuestItems},
// Part 3, second half: the create function 0x017DE870 itself starts with
// CMP dword ptr [RAX + 0x124], 0x6 ; JZ 0x017DEB34 (return) (0F 84 5C020000 at 0x017DE8D2)
// A zero displacement lands the jump on the next instruction, so the guest's item is made.
// It then only broadcasts the new item when hosting (+0x124 == 3), so a guest sends nothing.
{"GuestItems.CreateOwn", 0x017DE8D4, {0x5C, 0x02, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00},
Group::GuestItems},
// Part 5: placing the map's treasures, 0x017DC300 (see ByteEdits, part 5):
// CMP dword ptr [RAX + 0x124], 0x6 ; JZ 0x017DD081 (0F 84 A50C0000 at 0x017DC3D6)
// CMP dword ptr [RDX + 0x98], -1 ; JNZ 0x017DD081 (0F 85 980C0000 at 0x017DC3E3)
// 0x017DD081 is the end of the function: a guest placed no treasure at all. Zero
// displacements fall through, so the guest's game places them from its own map load. Which
// are shown is decided by the pickup flags (guest loot view in bloodborne_re.cpp); the
// character type table 0x047348A0 used right after allows type 1 (co-op phantom).
{"GuestItems.PlaceTreasures", 0x017DC3D8, {0xA5, 0x0C, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00},
Group::GuestItems},
{"GuestItems.PlaceTreasuresAway", 0x017DC3E5, {0x98, 0x0C, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00}, Group::GuestItems},
// Report 2026-10-07: "cannot use the bells in the Dream". The item availability check 0x0157F200
// gives the bells their own branches by goods id: CMP R12D, 0xCD / 0xE1 (Small Resonant /
// Sinister Bell) and at 0x0157F855 CMP EBX, 0xC8 (Beckoning Bell, 81 FB C8000000) - the branch
// that holds the seamless patches letting a host ring it where vanilla says no (0x0157F8C8,
// 0x0157F960). The kit's Hunt Bell is goods 4016 (a copy of 200), so it never got there; the
// vanilla bell itself is disabled by the kit. Its id goes into the compare: 4016 = 0x00000FB0.
// (The Kin Bell already works in the Dream through the general path - left as it is.)
{"KitBells.HuntBellChecks", 0x0157F857, {0xC8, 0x00, 0x00, 0x00}, {0xB0, 0x0F, 0x00, 0x00},
Group::KitBells},
// Three players, 2026-10-08 (host hardwater, guest cls_amg; abood's game had crashed but his
// sign was still listed): the host summoned the dead sign and sat in CreateWait for 6 minutes,
// then its own game raised "OnIrregularLeaveSession" (0x017897DA) and ended the session for the
// guest too. That is the reliable-packet send 0x017894E0: a packet it cannot deliver to a member
// is queued (list [SprjSessionManager]+0x188, counts at +0x30, plus +0x1B0), and once
// CMP EAX, 0x3E9 <- 0x017897AD (3D E9 03 00 00)
// 1001 are waiting it leaves. A member that never connects fills it at ~3 a second. The limit
// goes to 30000 (~2-3 hours); "BBSC send backlog:" lines log a queue that keeps growing.
{"SendBacklog.Limit", 0x017897AE, {0xE9, 0x03, 0x00, 0x00}, {0x30, 0x75, 0x00, 0x00},
Group::SendBacklog},
// Five hunters, part 2 (test 2026-10-08 on mod 37: with Hunters.KeepSearchingCoop the host's
// sign lists DID hold a third co-op sign - 3 signs, 2742-2745 bytes - but no summon was ever
// built for it). After the search, the same slot table decides which sign types may be taken:
// both co-op slots taken -> MOV ESI, 0xFFF7FF7F (co-op types masked out), else 0xFFFFFFFF
// at 0x0187304A (sign search 0x01872360) and 0x01879382 (0x01878D90). The mask stays all-ones;
// the summon builder's own cap (4 cooperators) still decides who gets in.
{"Hunters.CoopMaskSearch", 0x0187304B, {0x7F, 0xFF, 0xF7, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
Group::Hunters},
{"Hunters.CoopMaskSelect", 0x01879383, {0x7F, 0xFF, 0xF7, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
Group::Hunters},
}};
// Function entries replaced by a jmp to a sibling function with the same arguments.
struct EntryJump {
const char* name;
u64 site;
std::array<u8, 5> prologue; // expected first 5 bytes (whole instructions)
u64 target;
Group group;
};
constexpr std::array<EntryJump, 1> EntryJumps{{
// Lua_MultiWall(id) (0x0138B720) shows the co-op fog wall entity: 0x013CABD0(id, 1) draw on,
// 0x013CA580(id, 1) collision on, 0x01315FA0(id + 1) its effect. Lua_InvalidMultiWall (0x0138B890)
// is the same function with 0 / 0x01315E50, i.e. hides the wall. Jumping there keeps every wall
// hidden and walkable (CONFIRMED statically from both listings, 2026-09-17).
{"Lua_MultiWall", 0x0138B720, {0x55, 0x48, 0x89, 0xE5, 0x53}, 0x0138B890, Group::NoFogWalls},
}};
// ---- Which groups are on (same switches in the emulator and the mod DLL) ----
inline bool EnvFlag(const char* name) {
return SwitchEnabled(name);
}
inline bool GroupEnabled(Group group) {
switch (group) {
case Group::Stage:
case Group::Revive:
return true;
case Group::Boss:
return EnvFlag("SHADPS4_BBSC_PERSIST_BOSS");
case Group::MatchCheck:
return EnvFlag("SHADPS4_BBSC_PERSIST_MATCHCHECK");
case Group::Lamp:
return EnvFlag("SHADPS4_BBSC_LAMP");
case Group::Tasks:
return EnvFlag("SHADPS4_BBSC_PERSIST_TASKS");
case Group::HostDead:
return EnvFlag("SHADPS4_BBSC_PERSIST_HOSTDEAD");
case Group::NoFogWalls:
return EnvFlag("SHADPS4_BBSC_NO_FOG_WALLS");
case Group::LeaveLimit:
return EnvFlag("SHADPS4_BBSC_SKIP_LEAVE_LIMIT");
case Group::Spectate:
return EnvFlag("SHADPS4_BBSC_SPECTATE");
case Group::Echoes:
return EnvFlag("SHADPS4_BBSC_FULL_ECHOES");
case Group::GuestItems:
return EnvFlag("SHADPS4_BBSC_GUEST_ITEMS");
case Group::SolidNpcs:
return EnvFlag("SHADPS4_BBSC_SOLID_NPCS");
case Group::Scaling:
return EnvFlag("SHADPS4_BBSC_SCALING");
case Group::KitBells:
return true; // the kit is always given (bloodborne_re.cpp, PrepareBbscKitRows)
case Group::SendBacklog:
return true;
case Group::Hunters:
return EnvFlag("SHADPS4_BBSC_FIVE_HUNTERS");
}
return false;
}
// Groups a suspension (quit to title, partner gone) switches off so the vanilla teardown can run.
// Only the ones that keep a session alive: the echo fix, the lamp fix, the fog walls, the death
// reload retarget and host-death handling have nothing to do with ending a session, and switching
// them off for 20 s halved echoes, blocked lamps and let fog walls appear (code review B2).
inline bool IsSessionGroup(Group group) {
switch (group) {
case Group::Stage:
case Group::Revive:
case Group::Boss:
case Group::MatchCheck:
case Group::Tasks:
return true;
default:
return false;
}
}
// The lamp edits follow the player's role at runtime instead of being on all the time: a guest
// must never get the lamp prompt (vanilla never allows it, and a guest's lamp travel starts a load
// in the host's world), and single player must keep the vanilla gates (code review B3/B4).
inline bool IsRoleGroup(Group group) {
return group == Group::Lamp;
}
enum class Scope : u8 { Install, Session, Role };
inline bool InScope(Group group, Scope scope) {
switch (scope) {
case Scope::Install:
return !IsRoleGroup(group); // role groups start in their vanilla state
case Scope::Session:
return IsSessionGroup(group);
case Scope::Role:
return IsRoleGroup(group);
}
return false;
}
inline std::array<u8, 5> EncodeRel32(u8 opcode, u64 site, u64 target) {
const std::int64_t displacement = static_cast<std::int64_t>(target) - static_cast<std::int64_t>(site + 5);
const std::int32_t rel = static_cast<std::int32_t>(displacement);
std::array<u8, 5> bytes{opcode};
std::memcpy(bytes.data() + 1, &rel, sizeof(rel));
return bytes;
}
} // namespace Core::Bbsc::PatchTables
+263
View File
@@ -0,0 +1,263 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#include "core/bbsc_persistence.h"
#include "core/bbsc_patch_tables.h"
#include <array>
#include <cstdlib>
#include <cstring>
#include <string_view>
#include "common/logging/log.h"
#include "common/memory_patcher.h"
#include "common/singleton.h"
#include "common/types.h"
#include "core/file_format/psf.h"
namespace Core::Bbsc {
namespace {
using namespace PatchTables;
// Set once every enabled site has been byte-verified, so the runtime toggle can never write into
// an image the patches were rejected for.
bool patches_verified = false;
bool session_patches_on = false;
bool role_patches_on = false;
// Writes every enabled site in `scope` in its patched or its original form. Returns how many were
// written.
int WritePatches(bool enable, Scope scope) {
const u64 base = MemoryPatcher::g_eboot_address;
if (!patches_verified || base == 0) {
return 0;
}
if (scope == Scope::Session && session_patches_on == enable) {
return 0;
}
if (scope == Scope::Role && role_patches_on == enable) {
return 0;
}
int count = 0;
for (const auto& entry : EntryJumps) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
auto* site = reinterpret_cast<u8*>(base + entry.site);
if (enable) {
const auto patched = EncodeRel32(0xE9, entry.site, entry.target);
std::memcpy(site, patched.data(), patched.size());
} else {
std::memcpy(site, entry.prologue.data(), entry.prologue.size());
}
++count;
}
for (const auto& entry : Retargets) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
const auto bytes = EncodeRel32(entry.opcode, entry.site,
enable ? entry.target : entry.original);
std::memcpy(reinterpret_cast<u8*>(base + entry.site), bytes.data(), bytes.size());
++count;
}
for (const auto& entry : EntryReturns) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
// Only the first byte changes: 0xC3 (ret) or the function's real first instruction byte.
*reinterpret_cast<u8*>(base + entry.site) = enable ? 0xC3 : entry.prologue[0];
++count;
}
for (const auto& entry : ByteEdits) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
const auto& bytes = enable ? entry.replacement : entry.original;
std::memcpy(reinterpret_cast<u8*>(base + entry.site), bytes.data(), bytes.size());
++count;
}
for (const auto& entry : WideEdits) {
if (!GroupEnabled(entry.group) || !InScope(entry.group, scope)) {
continue;
}
const auto& bytes = enable ? entry.replacement : entry.original;
std::memcpy(reinterpret_cast<u8*>(base + entry.site), bytes.data(), bytes.size());
++count;
}
if (scope == Scope::Install) {
session_patches_on = enable;
} else if (scope == Scope::Session) {
session_patches_on = enable;
} else {
role_patches_on = enable;
}
return count;
}
} // namespace
void SetPersistentSessionPatches(bool enabled) {
const int count = WritePatches(enabled, Scope::Session);
if (count != 0) {
LOG_INFO(Debug, "BBSC persistence {} ({} session sites; echoes, lamp, fog walls and "
"deaths untouched)",
enabled ? "re-applied" : "suspended", count);
}
}
void SetHostLampPatches(bool hosting) {
const int count = WritePatches(hosting, Scope::Role);
if (count != 0) {
LOG_INFO(Debug, "BBSC lamp: {} ({} sites)",
hosting ? "prompt allowed (hosting a session)"
: "vanilla gates (solo or guest - a guest never gets the lamp)",
count);
}
}
void InstallPersistentSessionPatches() {
static bool attempted = false;
if (attempted) {
return;
}
if (!EnvFlag("SHADPS4_BBSC_PERSIST")) {
LOG_INFO(Debug, "BBSC persistence declined: SHADPS4_BBSC_PERSIST is off");
return;
}
attempted = true;
if (MemoryPatcher::g_game_serial != "CUSA03173") {
LOG_INFO(Debug, "BBSC persistence declined: this is {}, not CUSA03173",
MemoryPatcher::g_game_serial.empty() ? "an unknown title"
: MemoryPatcher::g_game_serial);
return;
}
const auto* param_sfo = Common::Singleton<PSF>::Instance();
const std::string_view app_version = param_sfo->GetString("APP_VER").value_or("Unknown");
if (app_version != "01.09") {
LOG_ERROR(Debug, "BBSC persistence requires CUSA03173 01.09; loaded version is {}",
app_version);
return;
}
const u64 base = MemoryPatcher::g_eboot_address;
const u64 image_size = MemoryPatcher::g_eboot_image_size;
if (base == 0) {
LOG_ERROR(Debug, "BBSC persistence declined: the eboot image base is not known yet");
return;
}
// Verify every enabled site before writing any, so a mismatch leaves the game untouched.
for (const auto& entry : Retargets) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + 5 > image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto original = EncodeRel32(entry.opcode, entry.site, entry.original);
const auto patched = EncodeRel32(entry.opcode, entry.site, entry.target);
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (std::memcmp(actual, original.data(), 5) != 0 &&
std::memcmp(actual, patched.data(), 5) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
for (const auto& entry : EntryReturns) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + entry.prologue.size() > image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (actual[0] != 0xC3 &&
std::memcmp(actual, entry.prologue.data(), entry.prologue.size()) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
for (const auto& entry : EntryJumps) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + entry.prologue.size() > image_size || entry.target >= image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto patched = EncodeRel32(0xE9, entry.site, entry.target);
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (std::memcmp(actual, patched.data(), patched.size()) != 0 &&
std::memcmp(actual, entry.prologue.data(), entry.prologue.size()) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
for (const auto& entry : ByteEdits) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + entry.original.size() > image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (std::memcmp(actual, entry.original.data(), entry.original.size()) != 0 &&
std::memcmp(actual, entry.replacement.data(), entry.replacement.size()) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
for (const auto& entry : WideEdits) {
if (!GroupEnabled(entry.group)) {
continue;
}
if (entry.site + entry.original.size() > image_size) {
LOG_ERROR(Debug, "BBSC persistence site {} is outside the image", entry.name);
return;
}
const auto* actual = reinterpret_cast<const u8*>(base + entry.site);
if (std::memcmp(actual, entry.original.data(), entry.original.size()) != 0 &&
std::memcmp(actual, entry.replacement.data(), entry.replacement.size()) != 0) {
LOG_ERROR(Debug,
"BBSC persistence signature mismatch at {} {:#x}; no persistence patches "
"applied",
entry.name, entry.site);
return;
}
}
patches_verified = true;
const int applied = WritePatches(true, Scope::Install);
LOG_INFO(Debug,
"BBSC persistence enabled ({} sites; boss={}, matching-check={}, lamp={}, "
"keep-tasks={}, ignore-host-death={}, no-fog-walls={}, skip-leave-limit={}, "
"death-without-reload={}, guest-items={}, solid-npcs={})",
applied, GroupEnabled(Group::Boss), GroupEnabled(Group::MatchCheck),
GroupEnabled(Group::Lamp), GroupEnabled(Group::Tasks),
GroupEnabled(Group::HostDead), GroupEnabled(Group::NoFogWalls),
GroupEnabled(Group::LeaveLimit), GroupEnabled(Group::Spectate),
GroupEnabled(Group::GuestItems), GroupEnabled(Group::SolidNpcs));
}
} // namespace Core::Bbsc
+107
View File
@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstdint>
#include <cstdlib>
#include <string_view>
namespace Core::Bbsc {
// BBSC build defaults: the co-op features confirmed working in game (2026-09-16) are on without
// any environment variable, so the build runs straight from a launcher. Everything still only
// applies to CUSA03173 01.09 (checked where each feature installs). Experimental switches stay
// off, and so is the boot intro (SHADPS4_BBSC_INTRO=1 brings it back). Setting a variable always wins: "0" turns a default off, any other value turns it on.
inline constexpr std::array<std::string_view, 36> DefaultOnSwitches{
"SHADPS4_BBSC_SHARED_PROGRESS", // merge the host's progress into the guest's world (adds only)
"SHADPS4_BBSC_WARP_TOGETHER", // the guest follows the host through a lamp warp
"SHADPS4_BBSC_WORLD_SYNC", // ...and loads the host's map with the host's world flags
"SHADPS4_BBSC_LOAD_COVER", // the guest's summon/follow loads behind a smooth black fade
"SHADPS4_BBSC_FULL_ECHOES", // a summoned guest is paid the host's rate, not half
"SHADPS4_BBSC_GUEST_ITEMS", // a guest picks up and keeps items in the host's world
"SHADPS4_BBSC_SOLID_NPCS", // a guest sees NPCs solid, not as grey wandering ghosts
"SHADPS4_BBSC_FLOOR_GUARD", // no crash when a character's floor was unloaded
"SHADPS4_BBSC_DREAM", // a guest uses the Doll, workshop and messengers in the host's Dream
"SHADPS4_BBSC_DEATH_PENALTY", // a kept death still drops a bloodstain and the echoes
"SHADPS4_BBSC_TRACE_WALLS", // log which map objects get shown/made solid (fog walls)
"SHADPS4_BBSC_LAMP", // the lamp prompt works in co-op (clears the +0x4B gate)
"SHADPS4_BBSC_NO_HP_PENALTY", // clears the summoned guest's 30% max HP cut (effect 9006)
"SHADPS4_BBSC_NO_FOG_WALLS", // co-op area fog walls hidden, leaving the area allowed
"SHADPS4_BBSC_MESSAGES", // co-op texts that fit seamless play (no "Returning to your world")
"SHADPS4_BBSC_CONNECT_FIRST", // cross-map summon: join first, one load into host map
"SHADPS4_BBSC_WARP_JOIN", // after a cross-map join, one more real reload (lighting)
"SHADPS4_BLOODBORNE_SEAMLESS_COOP", // Wozzardman's seamless summon
"SHADPS4_BLOODBORNE_RE_TRACE", // installs the BBSC hooks (and the trace logs)
"SHADPS4_CAPTURE_BLOODBORNE_SUMMON", // summon capture files for collect-logs
"SHADPS4_BBSC_PERSIST", // keep the session through loads and deaths
"SHADPS4_BBSC_PERSIST_MATCHCHECK", // ignore the post-load matching check kick
"SHADPS4_BBSC_REJOIN", // guest rejoin / reinsert after a kept death
"SHADPS4_BBSC_PERSIST_TASKS", // keep member tasks across a kept load
"SHADPS4_BBSC_PERSIST_HOSTDEAD", // the guest stays when the host dies
"SHADPS4_BBSC_SPECTATE", // respawn in place / spectate, join teleport
"SHADPS4_BBSC_FREE_BELL", // the Beckoning Bell costs no Insight
"SHADPS4_BBSC_FIVE_HUNTERS", // the host may summon up to 4 guests (5 hunters)
"SHADPS4_BBSC_CINEMATIC", // F1: free camera for filming (keyboard + mouse)
"SHADPS4_BBSC_SCALING", // enemy/boss HP, damage, stagger per guest (like ERSC)
"SHADPS4_BBSC_INVADE_ANYWHERE", // every world open to invaders (Bell Maiden SpEffect 9020)
"SHADPS4_BBSC_SIGN_ANY_AREA", // the Kin Bell's sign is posted in any area (0x014B6366)
"SHADPS4_BBSC_AREA_LIST_CAP", // fewer messages/bloodstains per request (Panic 265 crash)
"SHADPS4_BBSC_WORLD_SYNC_ALL", // the host's flags for every area, not just the one it is in
"SHADPS4_BBSC_TIMELINE", // a short bbsc-timeline-*.txt of what happened, next to the logs
"SHADPS4_BBSC_BELLS_ANYWHERE", // the kit's bells ring anywhere (Dream too); role / Insight still count
};
// False in the vanilla build (CMake BBSC_VANILLA): the emulator carries no BBSC of its own and
// BBSC runs as the bbsc.dll mod, which compiles this header itself (BBSC_MOD) with BuiltIn true.
#if defined(BBSC_VANILLA) && !defined(BBSC_MOD)
inline constexpr bool BuiltIn = false;
#else
inline constexpr bool BuiltIn = true;
#endif
inline bool SwitchEnabled(const char* name) {
if constexpr (!BuiltIn) {
// Every BBSC feature is off, whatever the environment says. The three real opt-out
// switches count as set, so the emulator's own seamless-summon helpers (fast sign,
// placement header) stay out of the way of the mod's.
const std::string_view key{name};
return key == "SHADPS4_BBSC_NO_PLACEMENT_HEADER" || key == "SHADPS4_BBSC_NO_FAST_SIGN" ||
key == "SHADPS4_BBSC_NO_GUEST_HOOKS";
}
const char* value = std::getenv(name);
if (value == nullptr || value[0] == '\0') {
for (const std::string_view on : DefaultOnSwitches) {
if (on == name) {
return true;
}
}
return false;
}
return std::string_view{value} != "0";
}
// Return-only guest function used as the target for skipped remote-player cleanup calls.
// Observed by the RE trace as "BBSC.Persist.SkippedCleanup" to record every persistence decision.
inline constexpr std::uint64_t PersistNoOpOffset = 0x013001E0;
// Phase 1 (docs/PHASE1-DESIGN.md, Option A): keep Bloodborne's co-op session alive across
// structural session ends (stage loads, revive, optionally boss clear and OnMatchingCheck) by
// retargeting the call sites of CSMultiPlayMan::Stop and WorldChrMan remote cleanup.
// On by default (SHADPS4_BBSC_PERSIST=0 disables). CUSA03173 01.09 only; every site is byte-verified.
void InstallPersistentSessionPatches();
// Turns the applied patches off and on again at runtime. A deliberate quit to the title has to
// tear the session down the way the game intends - otherwise the other player keeps a phantom
// partner and has to close the game (reported 2026-09-20). Reverting the patches for a few
// seconds lets the vanilla teardown run, then they go back on. Safe to call repeatedly; does
// nothing when the patches were never applied.
void SetPersistentSessionPatches(bool enabled);
// The lamp edits (prompt gate +0x49 and the character gate) are only on while this player hosts a
// session. Called from the game tick whenever the role changes; the lamp stays vanilla in single
// player and for a guest.
void SetHostLampPatches(bool hosting);
} // namespace Core::Bbsc
+498
View File
@@ -0,0 +1,498 @@
// SPDX-FileCopyrightText: Copyright 2026 CLS_AMG
// SPDX-License-Identifier: GPL-2.0-or-later
// BBSC relay client for the emulator build - the emulator side of mod\bbsc_relay.cpp (same server,
// same frames: [u32 length]["SHAD" 0x40 + u32 sender + u32 session + the packet]).
//
// Why (2026-10-19, logs test-logs\2026-10-19-boss-session): the host (emulator build) rang the Duel
// Bell; the first guest (emulator build) got it over the P2P signaling, the third player (BBport +
// the mod) never did - his log has no "received the host's Duel Bell state: on" - so on his screen
// the others stayed friends (his hits went through them) while they could hit him. CONFIRMED: the
// emulator build never connected to the relay (no "Relay" line in either emulator log) and the mod
// cannot hear the emulator's signaling packets, so his mod took a stranger as its host by position
// ("our host is player 1136bcaf (10 hosting on the server, it stands 14.5 m from the host...)") and
// used that stranger's scaling, world flags, boss progress and PvP state.
//
// Sending: every BBSC packet the emulator sends to its signaling peers goes to the relay as well
// (np_signaling_helpers.cpp). Receiving: frames from the relay go to the same handlers the mod
// uses, with two limits that keep two emulator builds exactly as before - a guest ignores the
// relay while BBSC packets arrive over the signaling (its host is an emulator build), and life
// packets ("SHAD" 0x23) stay unhandled as the emulator's signaling receive always did.
#include "core/bbsc_relay_client.h"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdlib>
#include <filesystem>
#include <cstring>
#include <fstream>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector>
#include "common/logging/log.h"
#include "common/path_util.h"
#include "core/bbsc_persistence.h"
#include "core/bloodborne_re.h"
#include "core/libraries/np/np_signaling/np_signaling_bbsc.h"
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
using SOCKET = int;
constexpr SOCKET INVALID_SOCKET = -1;
inline int closesocket(SOCKET s) { return close(s); }
#define SD_BOTH SHUT_RDWR
inline unsigned long GetCurrentProcessId() { return static_cast<unsigned long>(getpid()); }
#endif
namespace Core::Bbsc {
namespace {
constexpr std::uint16_t DefaultRelayPort = 20444;
constexpr std::uint8_t FollowType = 0x20;
constexpr std::uint8_t ScaleType = 0x24;
constexpr std::uint8_t PvpType = 0x25;
constexpr std::uint8_t ProgressType = 0x26;
constexpr std::uint8_t HostBossType = 0x27;
constexpr std::uint8_t AimedPlacementType = 0x31;
constexpr std::uint8_t PeersType = 0x7F;
constexpr std::uint8_t EnvelopeType = 0x40;
constexpr size_t EnvelopeSize = 13;
constexpr std::int64_t SignalingFreshMs = 5'000;
constexpr size_t SendQueueLimit = 256 * 1024;
#pragma pack(push, 1)
struct FollowPacket {
std::uint8_t magic[4];
std::uint8_t type;
std::uint8_t kind;
std::uint32_t packed_map;
float x, y, z, heading;
std::uint32_t sequence;
std::int32_t lamp_point;
};
#pragma pack(pop)
static_assert(sizeof(FollowPacket) == 0x22);
std::atomic<bool> started{false};
std::uint32_t own_id = 0;
std::mutex send_mutex;
std::condition_variable send_cv;
std::vector<std::vector<char>> send_queue;
size_t send_queue_bytes = 0;
SOCKET relay = INVALID_SOCKET;
std::atomic<int> peers{0};
std::atomic<std::int64_t> signaling_ms{0};
std::atomic<std::int64_t> own_sign_user{0};
std::mutex dispatch_mutex;
std::int64_t NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
void Log(const std::string& text) {
LOG_INFO(Debug, "BBSC relay client: {}", text);
Core::Bloodborne::BbscTimelineEvent("RELAY", text);
}
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream in(path, std::ios::binary);
std::stringstream text;
text << in.rdbuf();
return text.str();
}
// "http://26.1.2.3:20443" as the value of the ss4.scej-network.jp key -> "26.1.2.3".
std::string HostFromOverrides(const std::string& json) {
const size_t key = json.find("ss4.scej-network.jp");
if (key == std::string::npos) {
return {};
}
const size_t colon = json.find(':', json.find('"', key + 1) + 1);
const size_t open = json.find('"', colon);
const size_t close = json.find('"', open + 1);
if (colon == std::string::npos || open == std::string::npos || close == std::string::npos) {
return {};
}
std::string url = json.substr(open + 1, close - open - 1);
if (const size_t scheme = url.find("://"); scheme != std::string::npos) {
url = url.substr(scheme + 3);
}
return url.substr(0, url.find_first_of(":/"));
}
// The same list as the mod's bbsc_http.cpp IsPublicServer: those servers run no BBSC relay.
bool IsPublicServer(const std::string& host) {
for (const char* known : {"thehuntersdream.com", "nodreamforthehunter.com", "shadps4.net",
"scej-network.jp"}) {
const size_t len = std::strlen(known);
if (host.size() >= len && host.compare(host.size() - len, len, known) == 0) {
return true;
}
}
return false;
}
// The game server the emulator's HTTP uses (http.cpp LoadHostOverrideState reads the same file).
bool FindServer(std::string& host, std::uint16_t& port) {
port = DefaultRelayPort;
if (const char* env = std::getenv("BBSC_RELAY"); env != nullptr && env[0] != '\0') {
std::string value = env;
if (const size_t colon = value.find(':'); colon != std::string::npos) {
port = static_cast<std::uint16_t>(std::atoi(value.c_str() + colon + 1));
value.resize(colon);
}
host = value;
return true;
}
std::filesystem::path path;
if (const char* env = std::getenv("SHADPS4_HTTP_HOST_OVERRIDES_JSON"); env && env[0]) {
path = env;
} else {
path = Common::FS::GetUserPath(Common::FS::PathType::UserDir) / "host_overrides.json";
}
host = HostFromOverrides(ReadFile(path));
return !host.empty();
}
std::uint32_t SessionKey() {
const std::int32_t state = Core::Bloodborne::GetBbscRelaySessionState();
if (state == 3) {
return own_id;
}
if (state == 4 || state == 6) {
return Core::Bloodborne::GetBbscRelayHost();
}
return 0;
}
std::uint32_t SendFrame(const void* data, std::uint32_t size) {
std::scoped_lock lock{send_mutex};
if (relay == INVALID_SOCKET) {
return 0;
}
const std::uint32_t key = SessionKey();
const std::uint32_t wrapped = static_cast<std::uint32_t>(EnvelopeSize) + size;
std::vector<char> frame(4 + wrapped);
std::memcpy(frame.data(), &wrapped, 4);
std::memcpy(frame.data() + 4, "SHAD", 4);
frame[8] = static_cast<char>(EnvelopeType);
std::memcpy(frame.data() + 9, &own_id, 4);
std::memcpy(frame.data() + 13, &key, 4);
std::memcpy(frame.data() + 4 + EnvelopeSize, data, size);
if (send_queue_bytes + frame.size() > SendQueueLimit) {
return static_cast<std::uint32_t>(peers.load()); // the server is stuck: drop, keep the link
}
send_queue_bytes += frame.size();
send_queue.push_back(std::move(frame));
send_cv.notify_all();
return static_cast<std::uint32_t>(peers.load());
}
void WriterThread() {
for (;;) {
std::vector<char> frame;
SOCKET s = INVALID_SOCKET;
{
std::unique_lock lock{send_mutex};
send_cv.wait(lock, [] { return !send_queue.empty(); });
frame = std::move(send_queue.front());
send_queue.erase(send_queue.begin());
send_queue_bytes -= frame.size();
s = relay;
}
if (s == INVALID_SOCKET) {
continue;
}
int left = static_cast<int>(frame.size());
const char* at = frame.data();
while (left > 0) {
// bbport: no SIGPIPE (it ends the game on Linux) when the relay went away mid-send.
#ifdef MSG_NOSIGNAL
const int sent = send(s, at, left, MSG_NOSIGNAL);
#else
const int sent = send(s, at, left, 0);
#endif
if (sent <= 0) {
shutdown(s, SD_BOTH); // the relay thread sees it in recv and reconnects
break;
}
at += sent;
left -= sent;
}
}
}
bool ReadAll(SOCKET s, void* data, int size) {
auto* at = static_cast<char*>(data);
while (size > 0) {
const int got = recv(s, at, size, 0);
if (got <= 0) {
return false;
}
at += got;
size -= got;
}
return true;
}
void Dispatch(const std::vector<std::uint8_t>& wrapped) {
if (wrapped.size() == 2 && wrapped[0] == PeersType) {
peers = wrapped[1];
return;
}
if (wrapped.size() < EnvelopeSize + 5 || std::memcmp(wrapped.data(), "SHAD", 4) != 0 ||
wrapped[4] != EnvelopeType) {
return; // an older BBSC without the envelope
}
std::uint32_t sender = 0;
std::uint32_t session = 0;
std::memcpy(&sender, wrapped.data() + 5, 4);
std::memcpy(&session, wrapped.data() + 9, 4);
const std::vector<std::uint8_t> frame(wrapped.begin() + EnvelopeSize, wrapped.end());
if (sender == own_id || std::memcmp(frame.data(), "SHAD", 4) != 0) {
return;
}
const std::int32_t state = Core::Bloodborne::GetBbscRelaySessionState();
const bool guest = state == 4 || state == 6;
const bool sender_hosting = session != 0 && session == sender;
const std::uint8_t type = frame[4];
if (type == FollowType && frame.size() >= 0x1E && sender_hosting) {
FollowPacket follow{};
std::memcpy(&follow, frame.data(), std::min(frame.size(), sizeof(follow)));
// Every host's position: the mod's host lock (bloodborne_re.cpp) picks ours from them.
Core::Bloodborne::NoteBbscRelayHost(sender, follow.packed_map, follow.x, follow.y,
follow.z);
}
if (type == AimedPlacementType) {
// A host rang a sign: ours -> that sender is the host we are joining.
const std::string text(frame.begin() + 5, frame.end());
const size_t bar = text.find('|');
const std::int64_t own = own_sign_user.load();
if (bar != std::string::npos && own != 0 &&
std::atoll(text.substr(0, bar).c_str()) == own && state != 3 && !guest) {
Core::Bloodborne::NoteBbscHostRangOurSign(sender);
Log(fmt::format("host {:08x} rang our sign (user {})", sender, own));
}
return;
}
if (type == PvpType && frame.size() >= 11 && frame[5] == 1) {
// A guest asks its host (us) for PvP on/off.
if (state == 3 && session == own_id) {
Core::Bloodborne::OnBbscPvpPacket(frame[5], frame[6]);
}
return;
}
// Everything below comes from a host for its guests.
if (!guest || !sender_hosting || sender != Core::Bloodborne::GetBbscRelayHost()) {
return;
}
if (NowMs() - signaling_ms.load() < SignalingFreshMs) {
return; // our host talks over the signaling (an emulator build): as before
}
if (type == FollowType && frame.size() >= 0x1E) {
FollowPacket follow{};
std::memcpy(&follow, frame.data(), std::min(frame.size(), sizeof(follow)));
Core::Bloodborne::OnBbscHostFollowPacket(follow.packed_map, follow.x, follow.y, follow.z,
follow.heading, follow.sequence,
frame.size() >= sizeof(follow) ? follow.lamp_point
: -1);
} else if (type == ScaleType && frame.size() >= 15) {
std::uint16_t percent[5]{};
std::memcpy(percent, frame.data() + 5, sizeof(percent));
Core::Bloodborne::OnBbscScalePacket(percent);
} else if (type == PvpType && frame.size() >= 11) {
Core::Bloodborne::OnBbscPvpPacket(frame[5], frame[6]);
} else if (type == ProgressType && frame.size() >= 6) {
const size_t count = std::min<size_t>(frame[5], (frame.size() - 6) / 4);
if (count != 0) {
std::vector<std::uint32_t> flags(count);
std::memcpy(flags.data(), frame.data() + 6, count * 4);
Core::Bloodborne::OnBbscProgressPacket(flags.data(), flags.size());
}
} else if (type == HostBossType && frame.size() >= 9) {
std::int32_t boss = -1;
std::memcpy(&boss, frame.data() + 5, 4);
Core::Bloodborne::OnBbscHostBossPacket(boss);
} else if (type == Libraries::Np::NpSignaling::kSignalingBbscWorldFlagsType) {
Core::Bloodborne::OnBbscWorldFlagsPacket(frame.data(),
static_cast<std::uint32_t>(frame.size()));
}
}
void RelayThread() {
#ifdef _WIN32
WSADATA wsa{};
WSAStartup(MAKEWORD(2, 2), &wsa);
#endif
std::string last_problem;
for (;;) {
std::string host;
std::uint16_t port = 0;
if (!FindServer(host, port)) {
if (last_problem != "no server") {
last_problem = "no server";
Log("no game server address (host_overrides.json / BBSC_RELAY) - retrying");
}
std::this_thread::sleep_for(std::chrono::seconds(10));
continue;
}
if (IsPublicServer(host)) {
if (last_problem != "public") {
last_problem = "public";
Log("public game server (" + host + ") - no BBSC relay there");
}
std::this_thread::sleep_for(std::chrono::seconds(30));
continue;
}
addrinfo hints{};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
addrinfo* found = nullptr;
SOCKET s = INVALID_SOCKET;
if (getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &found) == 0) {
s = socket(found->ai_family, found->ai_socktype, found->ai_protocol);
if (s != INVALID_SOCKET &&
connect(s, found->ai_addr, static_cast<int>(found->ai_addrlen)) != 0) {
closesocket(s);
s = INVALID_SOCKET;
}
freeaddrinfo(found);
}
if (s == INVALID_SOCKET) {
const std::string problem = "cannot reach " + host + ":" + std::to_string(port);
if (problem != last_problem) {
last_problem = problem;
Log(problem + " (game server not started, or one without the relay) - retrying "
"every 5 s");
}
std::this_thread::sleep_for(std::chrono::seconds(5));
continue;
}
#ifdef _WIN32
BOOL on = TRUE;
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<const char*>(&on), sizeof(on));
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, reinterpret_cast<const char*>(&on), sizeof(on));
const DWORD send_timeout_ms = 10'000;
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast<const char*>(&send_timeout_ms),
sizeof(send_timeout_ms));
#else
int on = 1;
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on));
setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on));
struct timeval tv{};
tv.tv_sec = 10;
tv.tv_usec = 0;
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
#endif
{
std::scoped_lock lock{send_mutex};
relay = s;
}
last_problem.clear();
Log("connected to " + host + ":" + std::to_string(port));
const std::int64_t connected_at = NowMs();
std::uint64_t frames_in = 0;
for (;;) {
std::uint32_t size = 0;
if (!ReadAll(s, &size, 4) || size > 64 * 1024) {
break;
}
std::vector<std::uint8_t> frame(size);
if (size != 0 && !ReadAll(s, frame.data(), static_cast<int>(size))) {
break;
}
++frames_in;
std::scoped_lock dispatch_lock{dispatch_mutex};
Dispatch(frame);
}
{
std::scoped_lock lock{send_mutex};
relay = INVALID_SOCKET;
send_queue.clear();
send_queue_bytes = 0;
}
shutdown(s, SD_BOTH);
closesocket(s);
peers = 0;
Log("connection lost after " + std::to_string((NowMs() - connected_at) / 1000) + " s, " +
std::to_string(frames_in) + " frames in - reconnecting");
std::this_thread::sleep_for(std::chrono::seconds(2));
}
}
} // namespace
void StartRelayClient(std::string_view serial) {
if (serial != "CUSA03173") {
return; // BBSC only knows the EU 1.09 game
}
// The emulator's own BBSC switched off (the mod runs in it instead: bbsc-launch sets these to
// 0) - the mod has its own relay link.
if (!Core::Bbsc::SwitchEnabled("SHADPS4_BLOODBORNE_RE_TRACE") ||
!Core::Bbsc::SwitchEnabled("SHADPS4_BBSC_PERSIST")) {
return;
}
if (const char* env = std::getenv("SHADPS4_BBSC_RELAY"); env != nullptr && env[0] == '0') {
Log("off (SHADPS4_BBSC_RELAY=0)");
return;
}
if (started.exchange(true)) {
return;
}
const auto seed = static_cast<std::uint64_t>(
std::chrono::high_resolution_clock::now().time_since_epoch().count());
own_id = static_cast<std::uint32_t>((seed ^ static_cast<std::uint64_t>(GetCurrentProcessId())) *
2654435761ull) |
1u;
std::thread(RelayThread).detach();
std::thread(WriterThread).detach();
}
std::uint32_t RelaySendPacket(const void* packet, std::uint32_t size) {
if (!started.load() || packet == nullptr || size < 5) {
return 0;
}
return SendFrame(packet, size);
}
void NoteSignalingBbscPacket() {
signaling_ms = NowMs();
}
void NoteOwnSignUserId(std::int64_t user_id) {
if (user_id > 0 && own_sign_user.exchange(user_id) != user_id) {
LOG_INFO(Debug, "BBSC relay client: our sign's user id is {}", user_id);
}
}
void RelaySendAimedPlacement(std::int64_t target_user, const std::string& placement) {
std::string frame = "SHAD";
frame.push_back(static_cast<char>(AimedPlacementType));
frame += std::to_string(target_user) + "|" + placement;
if (!started.load()) {
return;
}
const std::uint32_t reached = RelaySendPacket(frame.data(), static_cast<std::uint32_t>(frame.size()));
Log(fmt::format("ringing the sign of user {} - told it over the relay that we are its host "
"({} other player(s) on the relay)",
target_user, reached));
}
} // namespace Core::Bbsc
@@ -0,0 +1,87 @@
// Generated by gpu/bbnet20/bbsc/translations/gen_translations.py from its *.json - do not
// edit. {english, {es, fr, de, it, pt, ru}}
{u"<?joinName?> joined your game.", {u"<?joinName?> se ha unido a tu partida.", u"<?joinName?> a rejoint votre partie.", u"<?joinName?> ist deinem Spiel beigetreten.", u"<?joinName?> si è unito alla tua partita.", u"<?joinName?> entrou no seu jogo.", u"Игрок <?joinName?> вошёл в вашу игру."}},
{u"You joined <?hostName?>'s game.", {u"Te has unido a la partida de <?hostName?>.", u"Vous avez rejoint la partie de <?hostName?>.", u"Du bist dem Spiel von <?hostName?> beigetreten.", u"Ti sei unito alla partita di <?hostName?>.", u"Você entrou no jogo de <?hostName?>.", u"Вы вошли в игру игрока <?hostName?>."}},
{u"You died.", {u"Has muerto.", u"Vous êtes mort.", u"Du bist gestorben.", u"Sei morto.", u"Você morreu.", u"Вы погибли."}},
{u"The host died.\nYou stay in their game.", {u"El anfitrión ha muerto.\nSigues en su partida.", u"L'hôte est mort.\nVous restez dans sa partie.", u"Der Gastgeber ist gestorben.\nDu bleibst in seinem Spiel.", u"L'ospitante è morto.\nResti nella sua partita.", u"O anfitrião morreu.\nVocê continua no jogo dele.", u"Хозяин мира погиб.\nВы остаётесь в его игре."}},
{u"<?deadName?> died.\nThey will respawn.", {u"<?deadName?> ha muerto.\nVolverá a aparecer.", u"<?deadName?> est mort.\nIl va réapparaître.", u"<?deadName?> ist gestorben.\nEr kehrt gleich zurück.", u"<?deadName?> è morto.\nRitornerà in vita.", u"<?deadName?> morreu.\nVai renascer.", u"Игрок <?deadName?> погиб.\nОн возродится."}},
{u"The host <?hostName?> died.\nYou stay in their game.", {u"El anfitrión <?hostName?> ha muerto.\nSigues en su partida.", u"L'hôte <?hostName?> est mort.\nVous restez dans sa partie.", u"Gastgeber <?hostName?> ist gestorben.\nDu bleibst in seinem Spiel.", u"L'ospitante <?hostName?> è morto.\nResti nella sua partita.", u"O anfitrião <?hostName?> morreu.\nVocê continua no jogo dele.", u"Хозяин мира <?hostName?> погиб.\nВы остаётесь в его игре."}},
{u"The host started the battle against\n{boss}.", {u"El anfitrión ha iniciado el combate contra\n{boss}.", u"L'hôte a engagé le combat contre\n{boss}.", u"Der Gastgeber hat den Kampf gegen\n{boss} begonnen.", u"L'ospitante ha iniziato lo scontro con\n{boss}.", u"O anfitrião iniciou a batalha contra\n{boss}.", u"Хозяин мира вступил в бой:\n{boss}."}},
{u"The host started a boss battle.", {u"El anfitrión se enfrenta a un jefe.", u"L'hôte a engagé un combat de boss.", u"Der Gastgeber hat einen Bosskampf begonnen.", u"L'ospitante ha sfidato un boss.", u"O anfitrião iniciou uma batalha contra chefe.", u"Хозяин мира начал битву с боссом."}},
{u"{boss} defeated.", {u"Jefe derrotado: {boss}.", u"{boss} a été vaincu.", u"{boss} besiegt.", u"{boss} sconfitto.", u"{boss} foi derrotado.", u"Босс повержен: {boss}."}},
{u"Boss defeated.", {u"Jefe derrotado.", u"Boss vaincu.", u"Boss besiegt.", u"Boss sconfitto.", u"Chefe derrotado.", u"Босс повержен."}},
{u"Invader <?joinName?> entered your world!", {u"¡El invasor <?joinName?> ha entrado en tu mundo!", u"L'envahisseur <?joinName?> est arrivé !", u"Eindringling <?joinName?> ist in deine Welt eingedrungen!", u"L'invasore <?joinName?> è entrato nel tuo mondo!", u"O invasor <?joinName?> entrou no seu mundo!", u"Захватчик <?joinName?> вторгся в ваш мир!"}},
{u"A bell-ringing woman appeared.\nAn invader may come.", {u"Ha aparecido una mujer de la campana.\nPuede llegar un invasor.", u"Une sonneuse de cloche est apparue.\nUn envahisseur pourrait venir.", u"Eine Glockenfrau ist erschienen.\nEin Eindringling könnte kommen.", u"È apparsa una donna con la campana.\nPotrebbe arrivare un invasore.", u"Uma mulher do sino apareceu.\nUm invasor pode chegar.", u"Появилась женщина с колоколом.\nМожет прийти захватчик."}},
{u"Invader <?deadName?> was killed.", {u"El invasor <?deadName?> ha muerto.", u"L'envahisseur <?deadName?> a été tué.", u"Eindringling <?deadName?> wurde getötet.", u"L'invasore <?deadName?> è stato ucciso.", u"O invasor <?deadName?> foi morto.", u"Захватчик <?deadName?> убит."}},
{u"Invader <?leaveName?> left\nand went back to their world.", {u"El invasor <?leaveName?> se ha ido\ny ha vuelto a su mundo.", u"L'envahisseur <?leaveName?> est parti\net a regagné son monde.", u"Eindringling <?leaveName?> ist gegangen\nund in seine Welt zurückgekehrt.", u"L'invasore <?leaveName?> se n'è andato\ne è tornato nel suo mondo.", u"O invasor <?leaveName?> saiu\ne voltou ao próprio mundo.", u"Захватчик <?leaveName?> ушёл\nи вернулся в свой мир."}},
{u"You invaded <?hostName?>'s world.", {u"Has invadido el mundo de <?hostName?>.", u"Vous avez envahi le monde de <?hostName?>.", u"Du bist in die Welt von <?hostName?> eingedrungen.", u"Hai invaso il mondo di <?hostName?>.", u"Você invadiu o mundo de <?hostName?>.", u"Вы вторглись в мир игрока <?hostName?>."}},
{u"A player answered your bell.\nJoining...", {u"Un jugador ha respondido a tu campana.\nConectando...", u"Un joueur a répondu.\nConnexion...", u"Ein Spieler hat deine Glocke gehört.\nBeitritt läuft ...", u"Un giocatore ha risposto alla campana.\nIngresso in corso...", u"Um jogador respondeu ao seu sino.\nEntrando...", u"Игрок откликнулся на ваш колокол.\nПодключение..."}},
{u"Found a host.\nJoining their game...", {u"Anfitrión encontrado.\nUniéndote a su partida...", u"Hôte trouvé.\nConnexion à sa partie...", u"Gastgeber gefunden.\nTrete seinem Spiel bei ...", u"Ospitante trovato.\nIngresso nella sua partita...", u"Anfitrião encontrado.\nEntrando no jogo dele...", u"Хозяин мира найден.\nПодключение к его игре..."}},
{u"Ring the Hunt Bell\nto call other players?", {u"¿Tocar la Campana de cacería\npara llamar a otros jugadores?", u"Sonner la Cloche de chasse\npour appeler des joueurs ?", u"Die Jagdglocke läuten, um\nandere Spieler zu rufen?", u"Suonare la Campana della caccia\nper chiamare altri giocatori?", u"Tocar o Sino da Caçada\npara chamar outros jogadores?", u"Позвонить в Колокол охоты,\nчтобы позвать других игроков?"}},
{u"Stop the Hunt Bell", {u"Silenciar la Campana de cacería", u"Arrêter la Cloche de chasse", u"Jagdglocke verstummen lassen", u"Ferma la Campana della caccia", u"Parar o Sino da Caçada", u"Заглушить Колокол охоты"}},
{u"Stop the Kin Bell", {u"Silenciar la Campana fraterna", u"Arrêter la Cloche fraternelle", u"Sippenglocke verstummen lassen", u"Ferma la Campana fraterna", u"Parar o Sino Fraterno", u"Заглушить Колокол родства"}},
{u"Stop the Bloodlust Bell", {u"Silenciar la Campana sanguinaria", u"Arrêter la Cloche sanguinaire", u"Blutrauschglocke verstummen lassen", u"Ferma la Campana sanguinaria", u"Parar o Sino Sanguinário", u"Заглушить Колокол кровожадности"}},
{u"Hunt Bell rung.\nWaiting for players to join...", {u"Campana de cacería tocada.\nEsperando a que se unan jugadores...", u"Cloche de chasse sonnée.\nEn attente d'autres joueurs...", u"Jagdglocke geläutet.\nWarte auf Mitspieler ...", u"Campana della caccia suonata.\nIn attesa di altri giocatori...", u"Sino da Caçada tocado.\nAguardando outros jogadores...", u"Колокол охоты звонит.\nОжидание игроков..."}},
{u"Kin Bell rung.\nLooking for a host to join...", {u"Campana fraterna tocada.\nBuscando un anfitrión al que unirte...", u"Cloche fraternelle sonnée.\nRecherche d'un hôte à rejoindre...", u"Sippenglocke geläutet.\nSuche einen Gastgeber ...", u"Campana fraterna suonata.\nRicerca di un ospitante...", u"Sino Fraterno tocado.\nProcurando um anfitrião...", u"Колокол родства звонит.\nПоиск хозяина мира..."}},
{u"Bloodlust Bell rung.\nLooking for a world to invade...", {u"Campana sanguinaria tocada.\nBuscando un mundo que invadir...", u"Cloche sanguinaire sonnée.\nRecherche d'un monde à envahir...", u"Blutrauschglocke geläutet.\nSuche eine Welt zum Eindringen ...", u"Campana sanguinaria suonata.\nRicerca di un mondo da invadere...", u"Sino Sanguinário tocado.\nProcurando um mundo para invadir...", u"Колокол кровожадности звонит.\nПоиск мира для вторжения..."}},
{u"Nobody found.\nRing the bell again to keep looking.", {u"No se ha encontrado a nadie.\nVuelve a tocarla para seguir buscando.", u"Personne trouvé.\nResonnez la cloche pour continuer.", u"Niemanden gefunden.\nLäute die Glocke erneut, um weiterzusuchen.", u"Nessuno trovato.\nSuona di nuovo per continuare a cercare.", u"Ninguém encontrado.\nToque o sino de novo para continuar.", u"Никого не найдено.\nПозвоните снова, чтобы искать дальше."}},
{u"Someone is about to invade your world.", {u"Alguien está a punto de invadir tu mundo.", u"Quelqu'un s'apprête à envahir votre monde.", u"Jemand dringt gleich in deine Welt ein.", u"Qualcuno sta per invadere il tuo mondo.", u"Alguém está prestes a invadir seu mundo.", u"Кто-то готовится вторгнуться в ваш мир."}},
{u"Found a world to invade.\nEntering...", {u"Mundo que invadir encontrado.\nEntrando...", u"Monde à envahir trouvé.\nEntrée en cours...", u"Welt zum Eindringen gefunden.\nBetrete sie ...", u"Trovato un mondo da invadere.\nIngresso in corso...", u"Mundo para invadir encontrado.\nEntrando...", u"Мир для вторжения найден.\nВторжение..."}},
{u"Hunt Bell rung (password).\nWaiting for players to join...", {u"Campana de cacería tocada (contraseña).\nEsperando a que se unan jugadores...", u"Cloche de chasse sonnée (mot de passe).\nEn attente d'autres joueurs...", u"Jagdglocke geläutet (Passwort).\nWarte auf Mitspieler ...", u"Campana della caccia (password).\nIn attesa di altri giocatori...", u"Sino da Caçada tocado (senha).\nAguardando outros jogadores...", u"Колокол охоты звонит (пароль).\nОжидание игроков..."}},
{u"Kin Bell rung (password).\nLooking for a host to join...", {u"Campana fraterna tocada (contraseña).\nBuscando un anfitrión al que unirte...", u"Cloche fraternelle sonnée (mot de passe).\nRecherche d'un hôte à rejoindre...", u"Sippenglocke geläutet (Passwort).\nSuche einen Gastgeber ...", u"Campana fraterna (password).\nRicerca di un ospitante...", u"Sino Fraterno tocado (senha).\nProcurando um anfitrião...", u"Колокол родства звонит (пароль).\nПоиск хозяина мира..."}},
{u"Found a host,\nbut the beckoner is an Executioner, and thus an adversary.", {u"Anfitrión encontrado,\npero es un Verdugo y, por tanto, un adversario.", u"Hôte trouvé,\nmais c'est un Bourreau, et donc un adversaire.", u"Gastgeber gefunden,\ndoch der Rufende ist ein Henker und somit ein Gegner.", u"Ospitante trovato,\nma è un Giustiziere, e dunque un avversario.", u"Anfitrião encontrado,\nmas ele é um Executor e, portanto, um adversário.", u"Хозяин мира найден,\nно призвавший — Палач, а значит, противник."}},
{u"Found a host,\nbut the beckoner is a Vileblood, and thus an adversary.", {u"Anfitrión encontrado,\npero es de la Sangre Vil y, por tanto, un adversario.", u"Hôte trouvé,\nmais c'est un Sang-vicié, et donc un adversaire.", u"Gastgeber gefunden,\ndoch der Rufende ist ein Gemeinblut und somit ein Gegner.", u"Ospitante trovato,\nma è un Sangue Infetto, e dunque un avversario.", u"Anfitrião encontrado,\nmas ele é um Sangue Vil e, portanto, um adversário.", u"Хозяин мира найден,\nно призвавший — Нечистокровный, а значит, противник."}},
{u"Found a host,\nbut the beckoner is a blood-addled hunter, and thus an adversary.", {u"Anfitrión encontrado,\npero es un cazador ebrio de sangre y, por tanto, un adversario.", u"Hôte trouvé,\nmais c'est un chasseur ivre de sang, et donc un adversaire.", u"Gastgeber gefunden,\ndoch der Rufende ist ein blutberauschter Jäger und somit ein Gegner.", u"Ospitante trovato,\nma è un cacciatore ebbro di sangue, e dunque un avversario.", u"Anfitrião encontrado,\nmas ele é um caçador embriagado de sangue e, portanto, um adversário.", u"Хозяин мира найден,\nно призвавший — одурманенный кровью охотник, а значит, противник."}},
{u"Ring the Hunt Bell to call\nother players into your game.", {u"Toca la Campana de cacería para llamar\na otros jugadores a tu partida.", u"Sonnez la Cloche de chasse pour appeler\nd'autres joueurs dans votre partie.", u"Läute die Jagdglocke, um andere\nSpieler in dein Spiel zu rufen.", u"Suona la Campana della caccia per\nchiamare altri giocatori nella partita.", u"Toque o Sino da Caçada para chamar\noutros jogadores ao seu jogo.", u"Позвоните в Колокол охоты, чтобы\nпризвать других игроков в свою игру."}},
{u"Use the Parting Blank to end co-op.", {u"Usa el Cartucho de despedida para terminar el cooperativo.", u"La Cartouche d'adieu met fin à la coopération.", u"Beende das Koop mit der Abschiedspatrone.", u"La Cartuccia del commiato chiude la cooperativa.", u"Use o Cartucho de Despedida para encerrar o cooperativo.", u"Совместную игру завершает Прощальный патрон."}},
{u"Bloodlust Bell", {u"Campana sanguinaria", u"Cloche sanguinaire", u"Blutrauschglocke", u"Campana sanguinaria", u"Sino Sanguinário", u"Колокол кровожадности"}},
{u"Parting Blank", {u"Cartucho de despedida", u"Cartouche d'adieu", u"Abschiedspatrone", u"Cartuccia del commiato", u"Cartucho de Despedida", u"Прощальный патрон"}},
{u"End co-op", {u"Terminar cooperativo", u"Fin de la coopération", u"Koop beenden", u"Termina cooperativa", u"Encerrar cooperativo", u"Завершить совместную игру"}},
{u"End co-op and go home", {u"Terminar cooperativo y volver", u"Fin de la coopération et retour", u"Koop beenden und heimkehren", u"Termina cooperativa e torna a casa", u"Encerrar cooperativo e voltar", u"Завершить и вернуться домой"}},
{u"Quick search: find a host and join their game.", {u"Búsqueda rápida de un anfitrión al que unirte.", u"Recherche rapide : trouver un hôte et rejoindre sa partie.", u"Schnellsuche: Gastgeber finden und seinem Spiel beitreten.", u"Ricerca rapida: trova un ospitante e unisciti.", u"Busca rápida: ache um anfitrião e entre no jogo dele.", u"Быстрый поиск: найти хозяина мира и войти в его игру."}},
{u"Quick search: find a world to invade.", {u"Búsqueda rápida: encuentra un mundo que invadir.", u"Recherche rapide : trouver un monde à envahir.", u"Schnellsuche: Welt zum Eindringen finden.", u"Ricerca rapida: trova un mondo da invadere.", u"Busca rápida: encontre um mundo para invadir.", u"Быстрый поиск: найти мир для вторжения."}},
{u"Stop calling other players.", {u"Dejar de llamar a otros jugadores.", u"Cesser d'appeler d'autres joueurs.", u"Keine Spieler mehr rufen.", u"Smetti di chiamare altri giocatori.", u"Parar de chamar outros jogadores.", u"Перестать звать других игроков."}},
{u"Stop looking for a host.", {u"Dejar de buscar un anfitrión.", u"Cesser de chercher un hôte.", u"Suche nach Gastgeber beenden.", u"Smetti di cercare un ospitante.", u"Parar de procurar um anfitrião.", u"Прекратить поиск хозяина мира."}},
{u"Stop looking for a world to invade.", {u"Dejar de buscar un mundo que invadir.", u"Cesser de chercher un monde à envahir.", u"Suche nach einer Welt zum Eindringen beenden.", u"Smetti di cercare un mondo da invadere.", u"Parar de procurar um mundo para invadir.", u"Прекратить поиск мира для вторжения."}},
{u"Find a host and join their game?", {u"¿Buscar un anfitrión y unirte a su partida?", u"Trouver un hôte et rejoindre sa partie ?", u"Gastgeber finden und seinem Spiel beitreten?", u"Cercare un ospitante e unirsi a lui?", u"Encontrar um anfitrião e entrar no jogo dele?", u"Найти хозяина мира и войти в его игру?"}},
{u"Find a world to invade?", {u"¿Buscar un mundo que invadir?", u"Trouver un monde à envahir ?", u"Welt zum Eindringen finden?", u"Trovare un mondo da invadere?", u"Encontrar um mundo para invadir?", u"Найти мир для вторжения?"}},
{u"Need help? Ring the Hunt Bell.", {u"¿Necesitas ayuda? Toca la Campana de cacería.", u"Besoin d'aide ? Sonnez la Cloche de chasse.", u"Hilfe nötig? Läute die Jagdglocke.", u"Serve aiuto? Suona la Campana della caccia.", u"Precisa de ajuda? Toque o Sino da Caçada.", u"Нужна помощь? Позвоните в Колокол охоты."}},
{u"Call other players into your game\nwith the Hunt Bell.", {u"Llama a otros jugadores a tu partida\ncon la Campana de cacería.", u"Appelez d'autres joueurs dans votre partie\navec la Cloche de chasse.", u"Rufe mit der Jagdglocke andere\nSpieler in dein Spiel.", u"Chiama altri giocatori nella tua partita\ncon la Campana della caccia.", u"Chame outros jogadores ao seu jogo\ncom o Sino da Caçada.", u"Зовите других игроков в свою игру\nс помощью Колокола охоты."}},
{u"Join another player's game\nwith the Kin Bell.", {u"Únete a la partida de otro jugador\ncon la Campana fraterna.", u"Rejoignez la partie d'un autre joueur\navec la Cloche fraternelle.", u"Tritt mit der Sippenglocke dem Spiel\neines anderen Spielers bei.", u"Unisciti a un altro giocatore\ncon la Campana fraterna.", u"Entre no jogo de outro jogador\ncom o Sino Fraterno.", u"Входите в игру других игроков\nс помощью Колокола родства."}},
{u"You caught up with <?hostName?>.", {u"Has alcanzado a <?hostName?>.", u"Vous avez rattrapé <?hostName?>.", u"Du hast <?hostName?> eingeholt.", u"Hai raggiunto <?hostName?>.", u"Você alcançou <?hostName?>.", u"Вы догнали игрока <?hostName?>."}},
{u"<?joinName?> caught up with you.", {u"<?joinName?> te ha alcanzado.", u"<?joinName?> vous a rattrapé.", u"<?joinName?> hat dich eingeholt.", u"<?joinName?> ti ha raggiunto.", u"<?joinName?> alcançou você.", u"Игрок <?joinName?> догнал вас."}},
{u"You are in the host's Hunter's Dream.", {u"Estás en el Sueño del cazador del anfitrión.", u"Vous êtes dans le Rêve du chasseur de l'hôte.", u"Du bist im Traum des Jägers des Gastgebers.", u"Sei nel Sogno del cacciatore dell'ospitante.", u"Você está no Sonho do Caçador do anfitrião.", u"Вы во Сне охотника хозяина мира."}},
{u"Hunt Bell", {u"Campana de cacería", u"Cloche de chasse", u"Jagdglocke", u"Campana della caccia", u"Sino da Caçada", u"Колокол охоты"}},
{u"Ring to call other players into your game.\nCosts no Insight.", {u"Tócala para llamar a otros jugadores a tu partida.\nNo cuesta Lucidez.", u"Sonnez-la pour appeler d'autres joueurs.\nNe coûte aucune Intuition.", u"Läuten, um andere Spieler in dein Spiel zu rufen.\nKostet keine Einsicht.", u"Suonala per chiamare altri giocatori nella partita.\nNon consuma Intuizione.", u"Toque para chamar outros jogadores ao seu jogo.\nNão consome Discernimento.", u"Призывает других игроков в вашу игру.\nНе расходует Озарение."}},
{u"An old bell carried by the hunters of the\nworkshop, its toll heard across every world.\n\nRing it, and hunters who answer with a\nKin Bell may join your hunt. Together you\nwalk the night, through every lamp,\ndeath and dream, until the hunt is done.", {u"Vieja campana que portaban los cazadores del\ntaller, cuyo tañido se oye en todos los mundos.\n\nTócala, y los cazadores que respondan con una\nCampana fraterna podrán unirse a tu cacería.\nJuntos recorréis la noche, entre faroles,\nmuertes y sueños, hasta el fin de la cacería.", u"Vieille cloche des chasseurs de l'atelier,\ndont le glas résonne à travers les mondes.\n\nSonnez-la, et ceux qui répondent d'une\nCloche fraternelle rejoindront votre chasse.\nEnsemble, vous arpentez la nuit, de lanterne\nen mort et en rêve, jusqu'au bout de la chasse.", u"Eine alte Glocke der Jäger der Werkstatt,\nderen Schlag in allen Welten erklingt.\n\nLäute sie, und Jäger, die mit einer\nSippenglocke antworten, schließen sich\ndeiner Jagd an. Gemeinsam durchwandert\nihr die Nacht, durch jede Laterne, jeden\nTod und Traum, bis die Jagd vollendet ist.", u"Antica campana dei cacciatori della bottega,\nil cui rintocco si ode in ogni mondo.\n\nSuonala, e i cacciatori che rispondono con\nuna Campana fraterna si uniranno alla tua\ncaccia. Insieme attraverserete la notte,\nogni lanterna, morte e sogno, fino alla fine.", u"Um velho sino levado pelos caçadores da\noficina, cujo dobre ecoa em todos os mundos.\n\nToque-o, e caçadores que responderem com um\nSino Fraterno poderão se unir à sua caçada.\nJuntos, vocês percorrem a noite, por lampião,\nmorte e sonho, até que a caçada termine.", u"Старый колокол охотников мастерской;\nего звон слышен во всех мирах.\n\nПозвоните в него, и охотники, что ответят\nКолоколом родства, разделят вашу охоту.\nВместе вы пройдёте ночь — через каждый\nфонарь, смерть и сон, пока охота не кончится."}},
{u"Kin Bell", {u"Campana fraterna", u"Cloche fraternelle", u"Sippenglocke", u"Campana fraterna", u"Sino Fraterno", u"Колокол родства"}},
{u"Ring to join another players game.\nCosts no Insight.", {u"Tócala para unirte a la partida de otro jugador.\nNo cuesta Lucidez.", u"Sonnez-la pour rejoindre la partie d'un autre.\nNe coûte aucune Intuition.", u"Läuten, um dem Spiel eines anderen Spielers beizutreten.\nKostet keine Einsicht.", u"Suonala per unirti alla partita di un altro.\nNon consuma Intuizione.", u"Toque para entrar no jogo de outro jogador.\nNão consome Discernimento.", u"Позволяет войти в игру другого игрока.\nНе расходует Озарение."}},
{u"A small bell of the choir, its chime\nfaint, but true.\n\nHunters who hear a Hunt Bell may answer\nwith it, crossing into another world to\nstand at the side of kin. What is slain\nthere is slain for both.", {u"Pequeña campana del coro, de tañido\ndébil, pero fiel.\n\nLos cazadores que oyen una Campana de cacería\npueden responder con ella y cruzar a otro mundo\npara luchar junto a los suyos. Lo que allí\ncae, cae para ambos.", u"Petite cloche du Chœur, au tintement\nfaible, mais sincère.\n\nLes chasseurs qui entendent une Cloche de\nchasse peuvent y répondre et passer dans\nun autre monde, aux côtés des leurs. Ce qui\ny est abattu l'est pour tous deux.", u"Eine kleine Glocke des Chors, ihr Klang\nleise, doch aufrichtig.\n\nJäger, die eine Jagdglocke hören, können\nmit ihr antworten, in eine andere Welt\nhinübertreten und der Sippe beistehen.\nWas dort erschlagen wird, ist für\nbeide erschlagen.", u"Piccola campana del Coro, dal rintocco\nflebile, ma sincero.\n\nChi ode una Campana della caccia può\nrispondere con essa, varcando un altro\nmondo per stare al fianco dei fratelli.\nCiò che là viene ucciso, muore per entrambi.", u"Um pequeno sino do coro, de toque\nfraco, porém verdadeiro.\n\nCaçadores que ouvem um Sino da Caçada podem\nrespondê-lo, cruzando para outro mundo para\nficar ao lado dos seus. O que ali é abatido\né abatido para ambos.", u"Малый колокол хора, чей звон\nслаб, но верен.\n\nУслышав Колокол охоты, охотник может\nответить им и перейти в иной мир, чтобы\nвстать плечом к плечу с родными. Всё, что\nпало там, пало для обоих."}},
{u"Ring to invade another players world.\nCosts no Insight.", {u"Tócala para invadir el mundo de otro jugador.\nNo cuesta Lucidez.", u"Sonnez-la pour envahir le monde d'un autre.\nNe coûte aucune Intuition.", u"Läuten, um in die Welt eines anderen Spielers einzudringen.\nKostet keine Einsicht.", u"Suonala per invadere il mondo di un altro.\nNon consuma Intuizione.", u"Toque para invadir o mundo de outro jogador.\nNão consome Discernimento.", u"Позволяет вторгнуться в мир другого игрока.\nНе расходует Озарение."}},
{u"A bell that sounds only for the blood-drunk.\n\nNo maiden need ring for its toll to be\nheard: every hunter of the night is prey,\nalone or in company, wherever they walk.\n\nThe hunt for hunters never ends.", {u"Campana que solo tañe para los ebrios de sangre.\n\nNo hace falta doncella alguna para que se oiga\nsu tañido: todo cazador de la noche es presa,\nsolo o en compañía, allá donde vaya.\n\nLa cacería de cazadores no termina jamás.", u"Cloche qui ne sonne que pour les ivres de sang.\n\nNulle sonneuse n'est requise pour que son\nglas soit entendu : tout chasseur de la nuit\nest une proie, seul ou accompagné, où qu'il aille.\n\nLa chasse aux chasseurs n'a pas de fin.", u"Eine Glocke, die nur für die Blutberauschten\nerklingt.\n\nKeine Glockenfrau muss sie läuten, damit\nihr Schlag gehört wird: Jeder Jäger der\nNacht ist Beute, allein oder in Begleitung,\nwohin er auch geht.\n\nDie Jagd auf Jäger endet niemals.", u"Campana che suona solo per gli ebbri di sangue.\n\nNessuna fanciulla deve suonarla perché il\nsuo rintocco sia udito: ogni cacciatore\nnella notte è una preda, solo o in compagnia,\novunque egli cammini.\n\nLa caccia ai cacciatori non ha mai fine.", u"Um sino que soa apenas para os ébrios de sangue.\n\nNenhuma donzela precisa tocá-lo para que seu\ndobre seja ouvido: todo caçador da noite é presa,\nsozinho ou acompanhado, onde quer que ande.\n\nA caçada aos caçadores nunca termina.", u"Колокол, звучащий лишь для опьянённых кровью.\n\nЧтобы его звон услышали, не нужна дева\nс колоколом: любой охотник ночи — добыча,\nодин или в компании, где бы он ни был.\n\nОхота на охотников не кончается никогда."}},
{u"Use to end co-op.\nGuests go home; the host sends everyone home.", {u"Úsalo para terminar el cooperativo.\nInvitado: vuelve a casa. Anfitrión: todos vuelven.", u"Met fin à la coopération.\nInvité : retour chez soi. Hôte : tous renvoyés.", u"Benutzen, um das Koop zu beenden.\nGäste kehren heim; der Gastgeber schickt alle heim.", u"Usala per terminare la cooperativa.\nOspite: torni a casa. Ospitante: tutti a casa.", u"Use para encerrar o cooperativo.\nConvidados voltam; o anfitrião devolve todos.", u"Завершает совместную игру.\nГость уходит домой, хозяин — отправляет всех."}},
{u"A blank of silent powder.\n\nIts inaudible burst breaks the resonance\nbinding hunters of different worlds, and\neach returns to their own night.", {u"Cartucho de pólvora silenciosa.\n\nSu estallido inaudible rompe la resonancia\nque une a cazadores de mundos distintos, y\ncada uno regresa a su propia noche.", u"Cartouche de poudre silencieuse.\n\nSa détonation inaudible brise la résonance\nqui lie les chasseurs de mondes différents,\net chacun regagne sa propre nuit.", u"Eine Patrone aus lautlosem Pulver.\n\nIhr unhörbarer Knall bricht die Resonanz,\ndie Jäger verschiedener Welten verbindet,\nund jeder kehrt in seine eigene Nacht\nzurück.", u"Cartuccia di polvere silenziosa.\n\nIl suo scoppio impercettibile spezza la\nrisonanza che lega cacciatori di mondi\ndiversi, e ognuno torna alla propria notte.", u"Um cartucho de pólvora silenciosa.\n\nSua explosão inaudível rompe a ressonância\nque une caçadores de mundos diferentes, e\ncada um retorna à sua própria noite.", u"Патрон с беззвучным порохом.\n\nЕго неслышный выстрел рвёт резонанс,\nсвязующий охотников разных миров, и\nкаждый возвращается в свою ночь."}},
{u"No longer used. Use the Hunt Bell.", {u"Ya no se usa. Usa la Campana de cacería.", u"Obsolète. Utilisez la Cloche de chasse.", u"Nicht mehr in Gebrauch. Nutze die Jagdglocke.", u"Non più in uso. Usa la Campana della caccia.", u"Fora de uso. Use o Sino da Caçada.", u"Устарел. Используйте Колокол охоты."}},
{u"No longer used. Use the Kin Bell.", {u"Ya no se usa. Usa la Campana fraterna.", u"Obsolète. Utilisez la Cloche fraternelle.", u"Nicht mehr in Gebrauch. Nutze die Sippenglocke.", u"Non più in uso. Usa la Campana fraterna.", u"Fora de uso. Use o Sino Fraterno.", u"Устарел. Используйте Колокол родства."}},
{u"No longer used. Use the Bloodlust Bell.", {u"Ya no se usa. Usa la Campana sanguinaria.", u"Obsolète. Utilisez la Cloche sanguinaire.", u"Nicht mehr in Gebrauch. Nutze die Blutrauschglocke.", u"Non più in uso. Usa la Campana sanguinaria.", u"Fora de uso. Use o Sino Sanguinário.", u"Устарел. Используйте Колокол кровожадности."}},
{u"No longer used. Use the Parting Blank.", {u"Ya no se usa. Usa el Cartucho de despedida.", u"Obsolète. Utilisez la Cartouche d'adieu.", u"Nicht mehr in Gebrauch. Nutze die Abschiedspatrone.", u"Non più in uso. Usa la Cartuccia del commiato.", u"Fora de uso. Use o Cartucho de Despedida.", u"Устарел. Используйте Прощальный патрон."}},
{u"Duel Bell", {u"Campana de duelo", u"Cloche de duel", u"Duellglocke", u"Campana del duello", u"Sino do Duelo", u"Колокол дуэли"}},
{u"Ring in co-op to turn on PvP between players.\nRing again to turn it off.", {u"Tócala en cooperativo para activar el PvP.\nVuelve a tocarla para desactivarlo.", u"En coopération, active le PvP entre joueurs.\nSonnez-la à nouveau pour le désactiver.", u"Im Koop läuten, um PvP zwischen Spielern zu aktivieren.\nErneut läuten, um es zu deaktivieren.", u"Suonala in cooperativa per attivare il PvP.\nSuonala di nuovo per disattivarlo.", u"No cooperativo, toque para ativar o PvP.\nToque de novo para desativá-lo.", u"В совместной игре включает PvP между игроками.\nПовторный звон выключает его."}},
{u"A cracked bell with a sour, discordant chime.\n\nRing it while hunting together, and the\nresonance binding the hunters turns to\nrivalry: they may strike one another as foes.\n\nRing it again to restore the old bond.\nOnly the beckoner decides; a cooperator\nwho rings it merely asks.", {u"Campana agrietada de tañido agrio y discordante.\n\nAl tocarla durante una cacería conjunta, la\nresonancia que une a los cazadores se torna\nrivalidad: podrán herirse como enemigos.\n\nVuelve a tocarla para restaurar el vínculo.\nSolo el anfitrión decide; el cooperador\nque la toca se limita a pedirlo.", u"Cloche fêlée au tintement aigre et discordant.\n\nSonnez-la en pleine chasse commune, et la\nrésonance qui lie les chasseurs vire à la\nrivalité : ils peuvent se frapper en ennemis.\n\nSonnez-la à nouveau pour rétablir le lien.\nSeul l'hôte décide ; le coopérateur\nqui la sonne ne fait que le demander.", u"Eine gesprungene Glocke, ihr Klang herb\nund misstönend.\n\nLäute sie auf gemeinsamer Jagd, und die\nResonanz, die die Jäger verbindet, wird zu\nRivalität: Sie dürfen einander als Feinde\nschlagen.\n\nLäute sie erneut, um das alte Band\nwiederherzustellen. Nur der Rufende\nentscheidet; ein Mitstreiter, der sie\nläutet, bittet lediglich darum.", u"Campana incrinata dal rintocco aspro e stonato.\n\nSuonala durante una caccia comune, e la\nrisonanza che lega i cacciatori si muta in\nrivalità: potranno colpirsi come nemici.\n\nSuonala di nuovo per ripristinare il legame.\nSolo l'ospitante decide; un cooperatore\nche la suona si limita a chiederlo.", u"Um sino rachado, de toque azedo e dissonante.\n\nToque-o durante uma caçada conjunta, e a\nressonância que une os caçadores vira\nrivalidade: podem golpear uns aos outros.\n\nToque-o de novo para restaurar o antigo laço.\nSó o anfitrião decide; o cooperador\nque o toca apenas pede.", u"Треснувший колокол с резким, нестройным звоном.\n\nЕсли позвонить в него на совместной охоте,\nрезонанс, что связывает охотников, обратится\nсоперничеством: они смогут разить друг друга.\n\nПозвоните вновь, чтобы вернуть прежние узы.\nРешает лишь призвавший; союзник,\nпозвонив в него, лишь просит об этом."}},
{u"You respawned at the host's lamp.", {u"Has reaparecido en el farol del anfitrión.", u"Vous êtes réapparu à la lanterne de l'hôte.", u"Du bist an der Laterne des Gastgebers wiedererwacht.", u"Sei rinato alla lanterna dell'ospitante.", u"Você renasceu no lampião do anfitrião.", u"Вы возродились у фонаря хозяина мира."}},
{u"You respawned next to the host.", {u"Has reaparecido junto al anfitrión.", u"Vous êtes réapparu près de l'hôte.", u"Du bist neben dem Gastgeber wiedererwacht.", u"Sei rinato accanto all'ospitante.", u"Você renasceu ao lado do anfitrião.", u"Вы возродились рядом с хозяином мира."}},
{u"You respawned at the lamp.", {u"Has reaparecido en el farol.", u"Vous êtes réapparu à la lanterne.", u"Du bist an der Laterne wiedererwacht.", u"Sei rinato alla lanterna.", u"Você renasceu no lampião.", u"Вы возродились у фонаря."}},
{u"You respawned.", {u"Has reaparecido.", u"Vous êtes réapparu.", u"Du bist wiedererwacht.", u"Sei rinato.", u"Você renasceu.", u"Вы возродились."}},
{u"The host died.\nThey will respawn at the lamp.", {u"El anfitrión ha muerto.\nReaparecerá en el farol.", u"L'hôte est mort.\nIl va réapparaître à la lanterne.", u"Der Gastgeber ist gestorben.\nEr erwacht an der Laterne wieder.", u"L'ospitante è morto.\nRinascerà alla lanterna.", u"O anfitrião morreu.\nEle vai renascer no lampião.", u"Хозяин мира погиб.\nОн возродится у фонаря."}},
{u"Boss reward added to your items.", {u"Recompensa del jefe añadida a tus objetos.", u"Récompense du boss ajoutée à vos objets.", u"Bossbelohnung zu deinen Gegenständen hinzugefügt.", u"Ricompensa del boss aggiunta agli oggetti.", u"Recompensa do chefe adicionada aos seus itens.", u"Награда за босса добавлена в инвентарь."}},
{u"Shared progress saved:\nthe host's progress is now in your world.", {u"Progreso compartido guardado:\nel progreso del anfitrión ya está en tu mundo.", u"Progression partagée sauvegardée :\ncelle de l'hôte est dans votre monde.", u"Gemeinsamer Fortschritt gespeichert:\nDer Fortschritt des Gastgebers ist nun in deiner Welt.", u"Progressi condivisi salvati:\nquelli dell'ospitante ora sono nel tuo mondo.", u"Progresso compartilhado salvo:\no progresso do anfitrião agora está no seu mundo.", u"Общий прогресс сохранён:\nпрогресс хозяина теперь в вашем мире."}},
{u"Shared progress saved.\nRest at a lamp to see all of it.", {u"Progreso compartido guardado.\nDescansa en un farol para verlo todo.", u"Progression partagée sauvegardée.\nReposez-vous à une lanterne pour la voir.", u"Gemeinsamer Fortschritt gespeichert.\nRaste an einer Laterne, um alles zu sehen.", u"Progressi condivisi salvati.\nRiposa a una lanterna per vederli tutti.", u"Progresso compartilhado salvo.\nDescanse num lampião para ver tudo.", u"Общий прогресс сохранён.\nОтдохните у фонаря, чтобы увидеть всё."}},
{u"BBSC items added:", {u"Objetos de BBSC añadidos:", u"Objets BBSC ajoutés :", u"BBSC-Gegenstände erhalten:", u"Oggetti BBSC aggiunti:", u"Itens do BBSC adicionados:", u"Добавлены предметы BBSC:"}},
{u"PvP is ON.\nPlayers can hurt each other.", {u"PvP ACTIVADO.\nLos jugadores pueden herirse entre sí.", u"PvP ACTIVÉ.\nLes joueurs peuvent se blesser.", u"PvP ist AN.\nSpieler können einander verletzen.", u"PvP ATTIVO.\nI giocatori possono ferirsi a vicenda.", u"PvP ATIVADO.\nOs jogadores podem se ferir.", u"PvP ВКЛЮЧЕНО.\nИгроки могут ранить друг друга."}},
{u"PvP is OFF.", {u"PvP DESACTIVADO.", u"PvP DÉSACTIVÉ.", u"PvP ist AUS.", u"PvP DISATTIVATO.", u"PvP DESATIVADO.", u"PvP ВЫКЛЮЧЕНО."}},
{u"PvP needs another player in your game.", {u"El PvP requiere otro jugador en tu partida.", u"Le PvP requiert un autre joueur dans la partie.", u"PvP braucht einen weiteren Spieler in deinem Spiel.", u"Il PvP richiede un altro giocatore in partita.", u"O PvP precisa de outro jogador no seu jogo.", u"Для PvP нужен ещё один игрок в вашей игре."}},
{u"Your own world was put back\n(the game had closed while you were a guest).\nRest at a lamp or reload to see it.", {u"Se ha restaurado tu propio mundo\n(el juego se cerró mientras eras invitado).\nDescansa en un farol o recarga para verlo.", u"Votre monde a été restauré\n(le jeu s'est fermé pendant que vous étiez invité).\nReposez-vous à une lanterne ou rechargez.", u"Deine eigene Welt wurde wiederhergestellt\n(das Spiel wurde geschlossen, während du Gast warst).\nRaste an einer Laterne oder lade neu, um sie zu sehen.", u"Il tuo mondo è stato ripristinato\n(il gioco si era chiuso mentre eri ospite).\nRiposa a una lanterna o ricarica per vederlo.", u"Seu próprio mundo foi restaurado\n(o jogo fechou enquanto você era convidado).\nDescanse num lampião ou recarregue para vê-lo.", u"Ваш собственный мир восстановлен\n(игра закрылась, пока вы были гостем).\nОтдохните у фонаря или перезапустите игру."}},
{u"An item was added to your items.", {u"Se ha añadido un objeto a tus objetos.", u"Un objet a été ajouté à vos objets.", u"Ein Gegenstand wurde zu deinen Gegenständen hinzugefügt.", u"Un oggetto è stato aggiunto agli oggetti.", u"Um item foi adicionado aos seus itens.", u"Предмет добавлен в инвентарь."}},
{u"Watching hunter {n} of {count}.\nTab / R1: next hunter.\nF: free camera.", {u"Observando al cazador {n} de {count}.\nTab / R1: siguiente cazador.\nF: cámara libre.", u"Vous suivez le chasseur {n} sur {count}.\nTab / R1 : chasseur suivant.\nF : caméra libre.", u"Du beobachtest Jäger {n} von {count}.\nTab / R1: nächster Jäger.\nF: freie Kamera.", u"Stai osservando il cacciatore {n} di {count}.\nTab / R1: cacciatore successivo.\nF: telecamera libera.", u"Observando o caçador {n} de {count}.\nTab / R1: próximo caçador.\nF: câmera livre.", u"Вы наблюдаете за охотником {n} из {count}.\nTab / R1: следующий охотник.\nF: свободная камера."}},
{u"Watching the living hunter.\nF: free camera.", {u"Observando al cazador con vida.\nF: cámara libre.", u"Vous suivez le chasseur encore en vie.\nF : caméra libre.", u"Du beobachtest den lebenden Jäger.\nF: freie Kamera.", u"Stai osservando il cacciatore ancora in vita.\nF: telecamera libera.", u"Observando o caçador vivo.\nF: câmera livre.", u"Вы наблюдаете за живым охотником.\nF: свободная камера."}},
{u"Watching hunter {n} of {count}.", {u"Observando al cazador {n} de {count}.", u"Vous suivez le chasseur {n} sur {count}.", u"Du beobachtest Jäger {n} von {count}.", u"Stai osservando il cacciatore {n} di {count}.", u"Observando o caçador {n} de {count}.", u"Вы наблюдаете за охотником {n} из {count}."}},
File diff suppressed because it is too large. Load diff
+87
View File
@@ -0,0 +1,87 @@
{
"<?joinName?> joined your game.": "<?joinName?> ist deinem Spiel beigetreten.",
"You joined <?hostName?>'s game.": "Du bist dem Spiel von <?hostName?> beigetreten.",
"You died.": "Du bist gestorben.",
"The host died.\\nYou stay in their game.": "Der Gastgeber ist gestorben.\\nDu bleibst in seinem Spiel.",
"<?deadName?> died.\\nThey will respawn.": "<?deadName?> ist gestorben.\\nEr kehrt gleich zurück.",
"The host <?hostName?> died.\\nYou stay in their game.": "Gastgeber <?hostName?> ist gestorben.\\nDu bleibst in seinem Spiel.",
"The host started the battle against\\n{boss}.": "Der Gastgeber hat den Kampf gegen\\n{boss} begonnen.",
"The host started a boss battle.": "Der Gastgeber hat einen Bosskampf begonnen.",
"{boss} defeated.": "{boss} besiegt.",
"Boss defeated.": "Boss besiegt.",
"Invader <?joinName?> entered your world!": "Eindringling <?joinName?> ist in deine Welt eingedrungen!",
"A bell-ringing woman appeared.\\nAn invader may come.": "Eine Glockenfrau ist erschienen.\\nEin Eindringling könnte kommen.",
"Invader <?deadName?> was killed.": "Eindringling <?deadName?> wurde getötet.",
"Invader <?leaveName?> left\\nand went back to their world.": "Eindringling <?leaveName?> ist gegangen\\nund in seine Welt zurückgekehrt.",
"You invaded <?hostName?>'s world.": "Du bist in die Welt von <?hostName?> eingedrungen.",
"A player answered your bell.\\nJoining...": "Ein Spieler hat deine Glocke gehört.\\nBeitritt läuft ...",
"Found a host.\\nJoining their game...": "Gastgeber gefunden.\\nTrete seinem Spiel bei ...",
"Ring the Hunt Bell\\nto call other players?": "Die Jagdglocke läuten, um\\nandere Spieler zu rufen?",
"Stop the Hunt Bell": "Jagdglocke verstummen lassen",
"Stop the Kin Bell": "Sippenglocke verstummen lassen",
"Stop the Bloodlust Bell": "Blutrauschglocke verstummen lassen",
"Hunt Bell rung.\\nWaiting for players to join...": "Jagdglocke geläutet.\\nWarte auf Mitspieler ...",
"Kin Bell rung.\\nLooking for a host to join...": "Sippenglocke geläutet.\\nSuche einen Gastgeber ...",
"Bloodlust Bell rung.\\nLooking for a world to invade...": "Blutrauschglocke geläutet.\\nSuche eine Welt zum Eindringen ...",
"Nobody found.\\nRing the bell again to keep looking.": "Niemanden gefunden.\\nLäute die Glocke erneut, um weiterzusuchen.",
"Someone is about to invade your world.": "Jemand dringt gleich in deine Welt ein.",
"Found a world to invade.\\nEntering...": "Welt zum Eindringen gefunden.\\nBetrete sie ...",
"Hunt Bell rung (password).\\nWaiting for players to join...": "Jagdglocke geläutet (Passwort).\\nWarte auf Mitspieler ...",
"Kin Bell rung (password).\\nLooking for a host to join...": "Sippenglocke geläutet (Passwort).\\nSuche einen Gastgeber ...",
"Found a host,\\nbut the beckoner is an Executioner, and thus an adversary.": "Gastgeber gefunden,\\ndoch der Rufende ist ein Henker und somit ein Gegner.",
"Found a host,\\nbut the beckoner is a Vileblood, and thus an adversary.": "Gastgeber gefunden,\\ndoch der Rufende ist ein Gemeinblut und somit ein Gegner.",
"Found a host,\\nbut the beckoner is a blood-addled hunter, and thus an adversary.": "Gastgeber gefunden,\\ndoch der Rufende ist ein blutberauschter Jäger und somit ein Gegner.",
"Ring the Hunt Bell to call\\nother players into your game.": "Läute die Jagdglocke, um andere\\nSpieler in dein Spiel zu rufen.",
"Use the Parting Blank to end co-op.": "Beende das Koop mit der Abschiedspatrone.",
"Bloodlust Bell": "Blutrauschglocke",
"Parting Blank": "Abschiedspatrone",
"End co-op": "Koop beenden",
"End co-op and go home": "Koop beenden und heimkehren",
"Quick search: find a host and join their game.": "Schnellsuche: Gastgeber finden und seinem Spiel beitreten.",
"Quick search: find a world to invade.": "Schnellsuche: Welt zum Eindringen finden.",
"Stop calling other players.": "Keine Spieler mehr rufen.",
"Stop looking for a host.": "Suche nach Gastgeber beenden.",
"Stop looking for a world to invade.": "Suche nach einer Welt zum Eindringen beenden.",
"Find a host and join their game?": "Gastgeber finden und seinem Spiel beitreten?",
"Find a world to invade?": "Welt zum Eindringen finden?",
"Need help? Ring the Hunt Bell.": "Hilfe nötig? Läute die Jagdglocke.",
"Call other players into your game\\nwith the Hunt Bell.": "Rufe mit der Jagdglocke andere\\nSpieler in dein Spiel.",
"Join another player's game\\nwith the Kin Bell.": "Tritt mit der Sippenglocke dem Spiel\\neines anderen Spielers bei.",
"You caught up with <?hostName?>.": "Du hast <?hostName?> eingeholt.",
"<?joinName?> caught up with you.": "<?joinName?> hat dich eingeholt.",
"You are in the host's Hunter's Dream.": "Du bist im Traum des Jägers des Gastgebers.",
"Hunt Bell": "Jagdglocke",
"Ring to call other players into your game.\\nCosts no Insight.": "Läuten, um andere Spieler in dein Spiel zu rufen.\\nKostet keine Einsicht.",
"An old bell carried by the hunters of the\\nworkshop, its toll heard across every world.\\n\\nRing it, and hunters who answer with a\\nKin Bell may join your hunt. Together you\\nwalk the night, through every lamp,\\ndeath and dream, until the hunt is done.": "Eine alte Glocke der Jäger der Werkstatt,\\nderen Schlag in allen Welten erklingt.\\n\\nLäute sie, und Jäger, die mit einer\\nSippenglocke antworten, schließen sich\\ndeiner Jagd an. Gemeinsam durchwandert\\nihr die Nacht, durch jede Laterne, jeden\\nTod und Traum, bis die Jagd vollendet ist.",
"Kin Bell": "Sippenglocke",
"Ring to join another players game.\\nCosts no Insight.": "Läuten, um dem Spiel eines anderen Spielers beizutreten.\\nKostet keine Einsicht.",
"A small bell of the choir, its chime\\nfaint, but true.\\n\\nHunters who hear a Hunt Bell may answer\\nwith it, crossing into another world to\\nstand at the side of kin. What is slain\\nthere is slain for both.": "Eine kleine Glocke des Chors, ihr Klang\\nleise, doch aufrichtig.\\n\\nJäger, die eine Jagdglocke hören, können\\nmit ihr antworten, in eine andere Welt\\nhinübertreten und der Sippe beistehen.\\nWas dort erschlagen wird, ist für\\nbeide erschlagen.",
"Ring to invade another players world.\\nCosts no Insight.": "Läuten, um in die Welt eines anderen Spielers einzudringen.\\nKostet keine Einsicht.",
"A bell that sounds only for the blood-drunk.\\n\\nNo maiden need ring for its toll to be\\nheard: every hunter of the night is prey,\\nalone or in company, wherever they walk.\\n\\nThe hunt for hunters never ends.": "Eine Glocke, die nur für die Blutberauschten\\nerklingt.\\n\\nKeine Glockenfrau muss sie läuten, damit\\nihr Schlag gehört wird: Jeder Jäger der\\nNacht ist Beute, allein oder in Begleitung,\\nwohin er auch geht.\\n\\nDie Jagd auf Jäger endet niemals.",
"Use to end co-op.\\nGuests go home; the host sends everyone home.": "Benutzen, um das Koop zu beenden.\\nGäste kehren heim; der Gastgeber schickt alle heim.",
"A blank of silent powder.\\n\\nIts inaudible burst breaks the resonance\\nbinding hunters of different worlds, and\\neach returns to their own night.": "Eine Patrone aus lautlosem Pulver.\\n\\nIhr unhörbarer Knall bricht die Resonanz,\\ndie Jäger verschiedener Welten verbindet,\\nund jeder kehrt in seine eigene Nacht\\nzurück.",
"No longer used. Use the Hunt Bell.": "Nicht mehr in Gebrauch. Nutze die Jagdglocke.",
"No longer used. Use the Kin Bell.": "Nicht mehr in Gebrauch. Nutze die Sippenglocke.",
"No longer used. Use the Bloodlust Bell.": "Nicht mehr in Gebrauch. Nutze die Blutrauschglocke.",
"No longer used. Use the Parting Blank.": "Nicht mehr in Gebrauch. Nutze die Abschiedspatrone.",
"Duel Bell": "Duellglocke",
"Ring in co-op to turn on PvP between players.\\nRing again to turn it off.": "Im Koop läuten, um PvP zwischen Spielern zu aktivieren.\\nErneut läuten, um es zu deaktivieren.",
"A cracked bell with a sour, discordant chime.\\n\\nRing it while hunting together, and the\\nresonance binding the hunters turns to\\nrivalry: they may strike one another as foes.\\n\\nRing it again to restore the old bond.\\nOnly the beckoner decides; a cooperator\\nwho rings it merely asks.": "Eine gesprungene Glocke, ihr Klang herb\\nund misstönend.\\n\\nLäute sie auf gemeinsamer Jagd, und die\\nResonanz, die die Jäger verbindet, wird zu\\nRivalität: Sie dürfen einander als Feinde\\nschlagen.\\n\\nLäute sie erneut, um das alte Band\\nwiederherzustellen. Nur der Rufende\\nentscheidet; ein Mitstreiter, der sie\\nläutet, bittet lediglich darum.",
"You respawned at the host's lamp.": "Du bist an der Laterne des Gastgebers wiedererwacht.",
"You respawned next to the host.": "Du bist neben dem Gastgeber wiedererwacht.",
"You respawned at the lamp.": "Du bist an der Laterne wiedererwacht.",
"You respawned.": "Du bist wiedererwacht.",
"The host died.\\nThey will respawn at the lamp.": "Der Gastgeber ist gestorben.\\nEr erwacht an der Laterne wieder.",
"Boss reward added to your items.": "Bossbelohnung zu deinen Gegenständen hinzugefügt.",
"Shared progress saved:\\nthe host's progress is now in your world.": "Gemeinsamer Fortschritt gespeichert:\\nDer Fortschritt des Gastgebers ist nun in deiner Welt.",
"Shared progress saved.\\nRest at a lamp to see all of it.": "Gemeinsamer Fortschritt gespeichert.\\nRaste an einer Laterne, um alles zu sehen.",
"BBSC items added:": "BBSC-Gegenstände erhalten:",
"PvP is ON.\\nPlayers can hurt each other.": "PvP ist AN.\\nSpieler können einander verletzen.",
"PvP is OFF.": "PvP ist AUS.",
"PvP needs another player in your game.": "PvP braucht einen weiteren Spieler in deinem Spiel.",
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Deine eigene Welt wurde wiederhergestellt\\n(das Spiel wurde geschlossen, während du Gast warst).\\nRaste an einer Laterne oder lade neu, um sie zu sehen.",
"An item was added to your items.": "Ein Gegenstand wurde zu deinen Gegenständen hinzugefügt.",
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Du beobachtest Jäger {n} von {count}.\\nTab / R1: nächster Jäger.\\nF: freie Kamera.",
"Watching the living hunter.\\nF: free camera.": "Du beobachtest den lebenden Jäger.\\nF: freie Kamera.",
"Watching hunter {n} of {count}.": "Du beobachtest Jäger {n} von {count}."
}
+87
View File
@@ -0,0 +1,87 @@
{
"<?joinName?> joined your game.": "<?joinName?> se ha unido a tu partida.",
"You joined <?hostName?>'s game.": "Te has unido a la partida de <?hostName?>.",
"You died.": "Has muerto.",
"The host died.\\nYou stay in their game.": "El anfitrión ha muerto.\\nSigues en su partida.",
"<?deadName?> died.\\nThey will respawn.": "<?deadName?> ha muerto.\\nVolverá a aparecer.",
"The host <?hostName?> died.\\nYou stay in their game.": "El anfitrión <?hostName?> ha muerto.\\nSigues en su partida.",
"The host started the battle against\\n{boss}.": "El anfitrión ha iniciado el combate contra\\n{boss}.",
"The host started a boss battle.": "El anfitrión se enfrenta a un jefe.",
"{boss} defeated.": "Jefe derrotado: {boss}.",
"Boss defeated.": "Jefe derrotado.",
"Invader <?joinName?> entered your world!": "¡El invasor <?joinName?> ha entrado en tu mundo!",
"A bell-ringing woman appeared.\\nAn invader may come.": "Ha aparecido una mujer de la campana.\\nPuede llegar un invasor.",
"Invader <?deadName?> was killed.": "El invasor <?deadName?> ha muerto.",
"Invader <?leaveName?> left\\nand went back to their world.": "El invasor <?leaveName?> se ha ido\\ny ha vuelto a su mundo.",
"You invaded <?hostName?>'s world.": "Has invadido el mundo de <?hostName?>.",
"A player answered your bell.\\nJoining...": "Un jugador ha respondido a tu campana.\\nConectando...",
"Found a host.\\nJoining their game...": "Anfitrión encontrado.\\nUniéndote a su partida...",
"Ring the Hunt Bell\\nto call other players?": "¿Tocar la Campana de cacería\\npara llamar a otros jugadores?",
"Stop the Hunt Bell": "Silenciar la Campana de cacería",
"Stop the Kin Bell": "Silenciar la Campana fraterna",
"Stop the Bloodlust Bell": "Silenciar la Campana sanguinaria",
"Hunt Bell rung.\\nWaiting for players to join...": "Campana de cacería tocada.\\nEsperando a que se unan jugadores...",
"Kin Bell rung.\\nLooking for a host to join...": "Campana fraterna tocada.\\nBuscando un anfitrión al que unirte...",
"Bloodlust Bell rung.\\nLooking for a world to invade...": "Campana sanguinaria tocada.\\nBuscando un mundo que invadir...",
"Nobody found.\\nRing the bell again to keep looking.": "No se ha encontrado a nadie.\\nVuelve a tocarla para seguir buscando.",
"Someone is about to invade your world.": "Alguien está a punto de invadir tu mundo.",
"Found a world to invade.\\nEntering...": "Mundo que invadir encontrado.\\nEntrando...",
"Hunt Bell rung (password).\\nWaiting for players to join...": "Campana de cacería tocada (contraseña).\\nEsperando a que se unan jugadores...",
"Kin Bell rung (password).\\nLooking for a host to join...": "Campana fraterna tocada (contraseña).\\nBuscando un anfitrión al que unirte...",
"Found a host,\\nbut the beckoner is an Executioner, and thus an adversary.": "Anfitrión encontrado,\\npero es un Verdugo y, por tanto, un adversario.",
"Found a host,\\nbut the beckoner is a Vileblood, and thus an adversary.": "Anfitrión encontrado,\\npero es de la Sangre Vil y, por tanto, un adversario.",
"Found a host,\\nbut the beckoner is a blood-addled hunter, and thus an adversary.": "Anfitrión encontrado,\\npero es un cazador ebrio de sangre y, por tanto, un adversario.",
"Ring the Hunt Bell to call\\nother players into your game.": "Toca la Campana de cacería para llamar\\na otros jugadores a tu partida.",
"Use the Parting Blank to end co-op.": "Usa el Cartucho de despedida para terminar el cooperativo.",
"Bloodlust Bell": "Campana sanguinaria",
"Parting Blank": "Cartucho de despedida",
"End co-op": "Terminar cooperativo",
"End co-op and go home": "Terminar cooperativo y volver",
"Quick search: find a host and join their game.": "Búsqueda rápida de un anfitrión al que unirte.",
"Quick search: find a world to invade.": "Búsqueda rápida: encuentra un mundo que invadir.",
"Stop calling other players.": "Dejar de llamar a otros jugadores.",
"Stop looking for a host.": "Dejar de buscar un anfitrión.",
"Stop looking for a world to invade.": "Dejar de buscar un mundo que invadir.",
"Find a host and join their game?": "¿Buscar un anfitrión y unirte a su partida?",
"Find a world to invade?": "¿Buscar un mundo que invadir?",
"Need help? Ring the Hunt Bell.": "¿Necesitas ayuda? Toca la Campana de cacería.",
"Call other players into your game\\nwith the Hunt Bell.": "Llama a otros jugadores a tu partida\\ncon la Campana de cacería.",
"Join another player's game\\nwith the Kin Bell.": "Únete a la partida de otro jugador\\ncon la Campana fraterna.",
"You caught up with <?hostName?>.": "Has alcanzado a <?hostName?>.",
"<?joinName?> caught up with you.": "<?joinName?> te ha alcanzado.",
"You are in the host's Hunter's Dream.": "Estás en el Sueño del cazador del anfitrión.",
"Hunt Bell": "Campana de cacería",
"Ring to call other players into your game.\\nCosts no Insight.": "Tócala para llamar a otros jugadores a tu partida.\\nNo cuesta Lucidez.",
"An old bell carried by the hunters of the\\nworkshop, its toll heard across every world.\\n\\nRing it, and hunters who answer with a\\nKin Bell may join your hunt. Together you\\nwalk the night, through every lamp,\\ndeath and dream, until the hunt is done.": "Vieja campana que portaban los cazadores del\\ntaller, cuyo tañido se oye en todos los mundos.\\n\\nTócala, y los cazadores que respondan con una\\nCampana fraterna podrán unirse a tu cacería.\\nJuntos recorréis la noche, entre faroles,\\nmuertes y sueños, hasta el fin de la cacería.",
"Kin Bell": "Campana fraterna",
"Ring to join another players game.\\nCosts no Insight.": "Tócala para unirte a la partida de otro jugador.\\nNo cuesta Lucidez.",
"A small bell of the choir, its chime\\nfaint, but true.\\n\\nHunters who hear a Hunt Bell may answer\\nwith it, crossing into another world to\\nstand at the side of kin. What is slain\\nthere is slain for both.": "Pequeña campana del coro, de tañido\\ndébil, pero fiel.\\n\\nLos cazadores que oyen una Campana de cacería\\npueden responder con ella y cruzar a otro mundo\\npara luchar junto a los suyos. Lo que allí\\ncae, cae para ambos.",
"Ring to invade another players world.\\nCosts no Insight.": "Tócala para invadir el mundo de otro jugador.\\nNo cuesta Lucidez.",
"A bell that sounds only for the blood-drunk.\\n\\nNo maiden need ring for its toll to be\\nheard: every hunter of the night is prey,\\nalone or in company, wherever they walk.\\n\\nThe hunt for hunters never ends.": "Campana que solo tañe para los ebrios de sangre.\\n\\nNo hace falta doncella alguna para que se oiga\\nsu tañido: todo cazador de la noche es presa,\\nsolo o en compañía, allá donde vaya.\\n\\nLa cacería de cazadores no termina jamás.",
"Use to end co-op.\\nGuests go home; the host sends everyone home.": "Úsalo para terminar el cooperativo.\\nInvitado: vuelve a casa. Anfitrión: todos vuelven.",
"A blank of silent powder.\\n\\nIts inaudible burst breaks the resonance\\nbinding hunters of different worlds, and\\neach returns to their own night.": "Cartucho de pólvora silenciosa.\\n\\nSu estallido inaudible rompe la resonancia\\nque une a cazadores de mundos distintos, y\\ncada uno regresa a su propia noche.",
"No longer used. Use the Hunt Bell.": "Ya no se usa. Usa la Campana de cacería.",
"No longer used. Use the Kin Bell.": "Ya no se usa. Usa la Campana fraterna.",
"No longer used. Use the Bloodlust Bell.": "Ya no se usa. Usa la Campana sanguinaria.",
"No longer used. Use the Parting Blank.": "Ya no se usa. Usa el Cartucho de despedida.",
"Duel Bell": "Campana de duelo",
"Ring in co-op to turn on PvP between players.\\nRing again to turn it off.": "Tócala en cooperativo para activar el PvP.\\nVuelve a tocarla para desactivarlo.",
"A cracked bell with a sour, discordant chime.\\n\\nRing it while hunting together, and the\\nresonance binding the hunters turns to\\nrivalry: they may strike one another as foes.\\n\\nRing it again to restore the old bond.\\nOnly the beckoner decides; a cooperator\\nwho rings it merely asks.": "Campana agrietada de tañido agrio y discordante.\\n\\nAl tocarla durante una cacería conjunta, la\\nresonancia que une a los cazadores se torna\\nrivalidad: podrán herirse como enemigos.\\n\\nVuelve a tocarla para restaurar el vínculo.\\nSolo el anfitrión decide; el cooperador\\nque la toca se limita a pedirlo.",
"You respawned at the host's lamp.": "Has reaparecido en el farol del anfitrión.",
"You respawned next to the host.": "Has reaparecido junto al anfitrión.",
"You respawned at the lamp.": "Has reaparecido en el farol.",
"You respawned.": "Has reaparecido.",
"The host died.\\nThey will respawn at the lamp.": "El anfitrión ha muerto.\\nReaparecerá en el farol.",
"Boss reward added to your items.": "Recompensa del jefe añadida a tus objetos.",
"Shared progress saved:\\nthe host's progress is now in your world.": "Progreso compartido guardado:\\nel progreso del anfitrión ya está en tu mundo.",
"Shared progress saved.\\nRest at a lamp to see all of it.": "Progreso compartido guardado.\\nDescansa en un farol para verlo todo.",
"BBSC items added:": "Objetos de BBSC añadidos:",
"PvP is ON.\\nPlayers can hurt each other.": "PvP ACTIVADO.\\nLos jugadores pueden herirse entre sí.",
"PvP is OFF.": "PvP DESACTIVADO.",
"PvP needs another player in your game.": "El PvP requiere otro jugador en tu partida.",
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Se ha restaurado tu propio mundo\\n(el juego se cerró mientras eras invitado).\\nDescansa en un farol o recarga para verlo.",
"An item was added to your items.": "Se ha añadido un objeto a tus objetos.",
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Observando al cazador {n} de {count}.\\nTab / R1: siguiente cazador.\\nF: cámara libre.",
"Watching the living hunter.\\nF: free camera.": "Observando al cazador con vida.\\nF: cámara libre.",
"Watching hunter {n} of {count}.": "Observando al cazador {n} de {count}."
}
+87
View File
@@ -0,0 +1,87 @@
{
"<?joinName?> joined your game.": "<?joinName?> a rejoint votre partie.",
"You joined <?hostName?>'s game.": "Vous avez rejoint la partie de <?hostName?>.",
"You died.": "Vous êtes mort.",
"The host died.\\nYou stay in their game.": "L'hôte est mort.\\nVous restez dans sa partie.",
"<?deadName?> died.\\nThey will respawn.": "<?deadName?> est mort.\\nIl va réapparaître.",
"The host <?hostName?> died.\\nYou stay in their game.": "L'hôte <?hostName?> est mort.\\nVous restez dans sa partie.",
"The host started the battle against\\n{boss}.": "L'hôte a engagé le combat contre\\n{boss}.",
"The host started a boss battle.": "L'hôte a engagé un combat de boss.",
"{boss} defeated.": "{boss} a été vaincu.",
"Boss defeated.": "Boss vaincu.",
"Invader <?joinName?> entered your world!": "L'envahisseur <?joinName?> est arrivé !",
"A bell-ringing woman appeared.\\nAn invader may come.": "Une sonneuse de cloche est apparue.\\nUn envahisseur pourrait venir.",
"Invader <?deadName?> was killed.": "L'envahisseur <?deadName?> a été tué.",
"Invader <?leaveName?> left\\nand went back to their world.": "L'envahisseur <?leaveName?> est parti\\net a regagné son monde.",
"You invaded <?hostName?>'s world.": "Vous avez envahi le monde de <?hostName?>.",
"A player answered your bell.\\nJoining...": "Un joueur a répondu.\\nConnexion...",
"Found a host.\\nJoining their game...": "Hôte trouvé.\\nConnexion à sa partie...",
"Ring the Hunt Bell\\nto call other players?": "Sonner la Cloche de chasse\\npour appeler des joueurs ?",
"Stop the Hunt Bell": "Arrêter la Cloche de chasse",
"Stop the Kin Bell": "Arrêter la Cloche fraternelle",
"Stop the Bloodlust Bell": "Arrêter la Cloche sanguinaire",
"Hunt Bell rung.\\nWaiting for players to join...": "Cloche de chasse sonnée.\\nEn attente d'autres joueurs...",
"Kin Bell rung.\\nLooking for a host to join...": "Cloche fraternelle sonnée.\\nRecherche d'un hôte à rejoindre...",
"Bloodlust Bell rung.\\nLooking for a world to invade...": "Cloche sanguinaire sonnée.\\nRecherche d'un monde à envahir...",
"Nobody found.\\nRing the bell again to keep looking.": "Personne trouvé.\\nResonnez la cloche pour continuer.",
"Someone is about to invade your world.": "Quelqu'un s'apprête à envahir votre monde.",
"Found a world to invade.\\nEntering...": "Monde à envahir trouvé.\\nEntrée en cours...",
"Hunt Bell rung (password).\\nWaiting for players to join...": "Cloche de chasse sonnée (mot de passe).\\nEn attente d'autres joueurs...",
"Kin Bell rung (password).\\nLooking for a host to join...": "Cloche fraternelle sonnée (mot de passe).\\nRecherche d'un hôte à rejoindre...",
"Found a host,\\nbut the beckoner is an Executioner, and thus an adversary.": "Hôte trouvé,\\nmais c'est un Bourreau, et donc un adversaire.",
"Found a host,\\nbut the beckoner is a Vileblood, and thus an adversary.": "Hôte trouvé,\\nmais c'est un Sang-vicié, et donc un adversaire.",
"Found a host,\\nbut the beckoner is a blood-addled hunter, and thus an adversary.": "Hôte trouvé,\\nmais c'est un chasseur ivre de sang, et donc un adversaire.",
"Ring the Hunt Bell to call\\nother players into your game.": "Sonnez la Cloche de chasse pour appeler\\nd'autres joueurs dans votre partie.",
"Use the Parting Blank to end co-op.": "La Cartouche d'adieu met fin à la coopération.",
"Bloodlust Bell": "Cloche sanguinaire",
"Parting Blank": "Cartouche d'adieu",
"End co-op": "Fin de la coopération",
"End co-op and go home": "Fin de la coopération et retour",
"Quick search: find a host and join their game.": "Recherche rapide : trouver un hôte et rejoindre sa partie.",
"Quick search: find a world to invade.": "Recherche rapide : trouver un monde à envahir.",
"Stop calling other players.": "Cesser d'appeler d'autres joueurs.",
"Stop looking for a host.": "Cesser de chercher un hôte.",
"Stop looking for a world to invade.": "Cesser de chercher un monde à envahir.",
"Find a host and join their game?": "Trouver un hôte et rejoindre sa partie ?",
"Find a world to invade?": "Trouver un monde à envahir ?",
"Need help? Ring the Hunt Bell.": "Besoin d'aide ? Sonnez la Cloche de chasse.",
"Call other players into your game\\nwith the Hunt Bell.": "Appelez d'autres joueurs dans votre partie\\navec la Cloche de chasse.",
"Join another player's game\\nwith the Kin Bell.": "Rejoignez la partie d'un autre joueur\\navec la Cloche fraternelle.",
"You caught up with <?hostName?>.": "Vous avez rattrapé <?hostName?>.",
"<?joinName?> caught up with you.": "<?joinName?> vous a rattrapé.",
"You are in the host's Hunter's Dream.": "Vous êtes dans le Rêve du chasseur de l'hôte.",
"Hunt Bell": "Cloche de chasse",
"Ring to call other players into your game.\\nCosts no Insight.": "Sonnez-la pour appeler d'autres joueurs.\\nNe coûte aucune Intuition.",
"An old bell carried by the hunters of the\\nworkshop, its toll heard across every world.\\n\\nRing it, and hunters who answer with a\\nKin Bell may join your hunt. Together you\\nwalk the night, through every lamp,\\ndeath and dream, until the hunt is done.": "Vieille cloche des chasseurs de l'atelier,\\ndont le glas résonne à travers les mondes.\\n\\nSonnez-la, et ceux qui répondent d'une\\nCloche fraternelle rejoindront votre chasse.\\nEnsemble, vous arpentez la nuit, de lanterne\\nen mort et en rêve, jusqu'au bout de la chasse.",
"Kin Bell": "Cloche fraternelle",
"Ring to join another players game.\\nCosts no Insight.": "Sonnez-la pour rejoindre la partie d'un autre.\\nNe coûte aucune Intuition.",
"A small bell of the choir, its chime\\nfaint, but true.\\n\\nHunters who hear a Hunt Bell may answer\\nwith it, crossing into another world to\\nstand at the side of kin. What is slain\\nthere is slain for both.": "Petite cloche du Chœur, au tintement\\nfaible, mais sincère.\\n\\nLes chasseurs qui entendent une Cloche de\\nchasse peuvent y répondre et passer dans\\nun autre monde, aux côtés des leurs. Ce qui\\ny est abattu l'est pour tous deux.",
"Ring to invade another players world.\\nCosts no Insight.": "Sonnez-la pour envahir le monde d'un autre.\\nNe coûte aucune Intuition.",
"A bell that sounds only for the blood-drunk.\\n\\nNo maiden need ring for its toll to be\\nheard: every hunter of the night is prey,\\nalone or in company, wherever they walk.\\n\\nThe hunt for hunters never ends.": "Cloche qui ne sonne que pour les ivres de sang.\\n\\nNulle sonneuse n'est requise pour que son\\nglas soit entendu : tout chasseur de la nuit\\nest une proie, seul ou accompagné, où qu'il aille.\\n\\nLa chasse aux chasseurs n'a pas de fin.",
"Use to end co-op.\\nGuests go home; the host sends everyone home.": "Met fin à la coopération.\\nInvité : retour chez soi. Hôte : tous renvoyés.",
"A blank of silent powder.\\n\\nIts inaudible burst breaks the resonance\\nbinding hunters of different worlds, and\\neach returns to their own night.": "Cartouche de poudre silencieuse.\\n\\nSa détonation inaudible brise la résonance\\nqui lie les chasseurs de mondes différents,\\net chacun regagne sa propre nuit.",
"No longer used. Use the Hunt Bell.": "Obsolète. Utilisez la Cloche de chasse.",
"No longer used. Use the Kin Bell.": "Obsolète. Utilisez la Cloche fraternelle.",
"No longer used. Use the Bloodlust Bell.": "Obsolète. Utilisez la Cloche sanguinaire.",
"No longer used. Use the Parting Blank.": "Obsolète. Utilisez la Cartouche d'adieu.",
"Duel Bell": "Cloche de duel",
"Ring in co-op to turn on PvP between players.\\nRing again to turn it off.": "En coopération, active le PvP entre joueurs.\\nSonnez-la à nouveau pour le désactiver.",
"A cracked bell with a sour, discordant chime.\\n\\nRing it while hunting together, and the\\nresonance binding the hunters turns to\\nrivalry: they may strike one another as foes.\\n\\nRing it again to restore the old bond.\\nOnly the beckoner decides; a cooperator\\nwho rings it merely asks.": "Cloche fêlée au tintement aigre et discordant.\\n\\nSonnez-la en pleine chasse commune, et la\\nrésonance qui lie les chasseurs vire à la\\nrivalité : ils peuvent se frapper en ennemis.\\n\\nSonnez-la à nouveau pour rétablir le lien.\\nSeul l'hôte décide ; le coopérateur\\nqui la sonne ne fait que le demander.",
"You respawned at the host's lamp.": "Vous êtes réapparu à la lanterne de l'hôte.",
"You respawned next to the host.": "Vous êtes réapparu près de l'hôte.",
"You respawned at the lamp.": "Vous êtes réapparu à la lanterne.",
"You respawned.": "Vous êtes réapparu.",
"The host died.\\nThey will respawn at the lamp.": "L'hôte est mort.\\nIl va réapparaître à la lanterne.",
"Boss reward added to your items.": "Récompense du boss ajoutée à vos objets.",
"Shared progress saved:\\nthe host's progress is now in your world.": "Progression partagée sauvegardée :\\ncelle de l'hôte est dans votre monde.",
"Shared progress saved.\\nRest at a lamp to see all of it.": "Progression partagée sauvegardée.\\nReposez-vous à une lanterne pour la voir.",
"BBSC items added:": "Objets BBSC ajoutés :",
"PvP is ON.\\nPlayers can hurt each other.": "PvP ACTIVÉ.\\nLes joueurs peuvent se blesser.",
"PvP is OFF.": "PvP DÉSACTIVÉ.",
"PvP needs another player in your game.": "Le PvP requiert un autre joueur dans la partie.",
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Votre monde a été restauré\\n(le jeu s'est fermé pendant que vous étiez invité).\\nReposez-vous à une lanterne ou rechargez.",
"An item was added to your items.": "Un objet a été ajouté à vos objets.",
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Vous suivez le chasseur {n} sur {count}.\\nTab / R1 : chasseur suivant.\\nF : caméra libre.",
"Watching the living hunter.\\nF: free camera.": "Vous suivez le chasseur encore en vie.\\nF : caméra libre.",
"Watching hunter {n} of {count}.": "Vous suivez le chasseur {n} sur {count}."
}
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
"""bbport: makes gpu/bbnet20/bbsc/core/bbsc_translations.inc from the translations of BBSC's texts.
Each <language>.json here maps BBSC's English text (as written in its C++ sources, "\\n" for a line
break; "{boss}" where a text names one boss with <?npcNameId@N?>; "{n}" and "{count}" for a
text's first and second number) to that language. The .inc is the table core/bbsc_i18n.cpp looks
them up in; run this after changing a .json:
python3 gpu/bbnet20/bbsc/translations/gen_translations.py
"""
import json
from pathlib import Path
HERE = Path(__file__).resolve().parent
OUT = HERE.parent / 'core/bbsc_translations.inc'
# The column order of bbsc_i18n.cpp's Language.
LANGUAGES = ('es', 'fr', 'de', 'it', 'pt', 'ru')
TAGS = ('<?joinName?>', '<?hostName?>', '<?deadName?>', '<?leaveName?>', '{boss}', '{n}', '{count}')
def literal(text):
"""A C++ u"" literal of text, whose line breaks are already written as \\n."""
return 'u"' + text.replace('\\', '\\\\').replace('\\\\n', '\\n').replace('"', '\\"') + '"'
def main():
tables = {lang: json.loads((HERE / f'{lang}.json').read_text(encoding='utf-8'))
for lang in LANGUAGES}
english = list(tables[LANGUAGES[0]])
lines = ['// Generated by gpu/bbnet20/bbsc/translations/gen_translations.py from its *.json - do not',
'// edit. {english, {' + ', '.join(LANGUAGES) + '}}']
for text in english:
if '\n' in text: # not one of BBSC's texts
continue
row = []
for lang in LANGUAGES:
translated = tables[lang].get(text)
if translated is None or '\n' in translated:
raise SystemExit(f'{lang}: no translation of {text!r}')
for tag in TAGS:
if text.count(tag) != translated.count(tag):
raise SystemExit(f'{lang}: {tag} differs in {text!r}')
row.append(literal(translated))
lines.append('{' + literal(text) + ', {' + ', '.join(row) + '}},')
OUT.write_text('\n'.join(lines) + '\n', encoding='utf-8')
print(f'{OUT}: {len(lines) - 2} texts, {len(LANGUAGES)} languages')
if __name__ == '__main__':
main()
+87
View File
@@ -0,0 +1,87 @@
{
"<?joinName?> joined your game.": "<?joinName?> si è unito alla tua partita.",
"You joined <?hostName?>'s game.": "Ti sei unito alla partita di <?hostName?>.",
"You died.": "Sei morto.",
"The host died.\\nYou stay in their game.": "L'ospitante è morto.\\nResti nella sua partita.",
"<?deadName?> died.\\nThey will respawn.": "<?deadName?> è morto.\\nRitornerà in vita.",
"The host <?hostName?> died.\\nYou stay in their game.": "L'ospitante <?hostName?> è morto.\\nResti nella sua partita.",
"The host started the battle against\\n{boss}.": "L'ospitante ha iniziato lo scontro con\\n{boss}.",
"The host started a boss battle.": "L'ospitante ha sfidato un boss.",
"{boss} defeated.": "{boss} sconfitto.",
"Boss defeated.": "Boss sconfitto.",
"Invader <?joinName?> entered your world!": "L'invasore <?joinName?> è entrato nel tuo mondo!",
"A bell-ringing woman appeared.\\nAn invader may come.": "È apparsa una donna con la campana.\\nPotrebbe arrivare un invasore.",
"Invader <?deadName?> was killed.": "L'invasore <?deadName?> è stato ucciso.",
"Invader <?leaveName?> left\\nand went back to their world.": "L'invasore <?leaveName?> se n'è andato\\ne è tornato nel suo mondo.",
"You invaded <?hostName?>'s world.": "Hai invaso il mondo di <?hostName?>.",
"A player answered your bell.\\nJoining...": "Un giocatore ha risposto alla campana.\\nIngresso in corso...",
"Found a host.\\nJoining their game...": "Ospitante trovato.\\nIngresso nella sua partita...",
"Ring the Hunt Bell\\nto call other players?": "Suonare la Campana della caccia\\nper chiamare altri giocatori?",
"Stop the Hunt Bell": "Ferma la Campana della caccia",
"Stop the Kin Bell": "Ferma la Campana fraterna",
"Stop the Bloodlust Bell": "Ferma la Campana sanguinaria",
"Hunt Bell rung.\\nWaiting for players to join...": "Campana della caccia suonata.\\nIn attesa di altri giocatori...",
"Kin Bell rung.\\nLooking for a host to join...": "Campana fraterna suonata.\\nRicerca di un ospitante...",
"Bloodlust Bell rung.\\nLooking for a world to invade...": "Campana sanguinaria suonata.\\nRicerca di un mondo da invadere...",
"Nobody found.\\nRing the bell again to keep looking.": "Nessuno trovato.\\nSuona di nuovo per continuare a cercare.",
"Someone is about to invade your world.": "Qualcuno sta per invadere il tuo mondo.",
"Found a world to invade.\\nEntering...": "Trovato un mondo da invadere.\\nIngresso in corso...",
"Hunt Bell rung (password).\\nWaiting for players to join...": "Campana della caccia (password).\\nIn attesa di altri giocatori...",
"Kin Bell rung (password).\\nLooking for a host to join...": "Campana fraterna (password).\\nRicerca di un ospitante...",
"Found a host,\\nbut the beckoner is an Executioner, and thus an adversary.": "Ospitante trovato,\\nma è un Giustiziere, e dunque un avversario.",
"Found a host,\\nbut the beckoner is a Vileblood, and thus an adversary.": "Ospitante trovato,\\nma è un Sangue Infetto, e dunque un avversario.",
"Found a host,\\nbut the beckoner is a blood-addled hunter, and thus an adversary.": "Ospitante trovato,\\nma è un cacciatore ebbro di sangue, e dunque un avversario.",
"Ring the Hunt Bell to call\\nother players into your game.": "Suona la Campana della caccia per\\nchiamare altri giocatori nella partita.",
"Use the Parting Blank to end co-op.": "La Cartuccia del commiato chiude la cooperativa.",
"Bloodlust Bell": "Campana sanguinaria",
"Parting Blank": "Cartuccia del commiato",
"End co-op": "Termina cooperativa",
"End co-op and go home": "Termina cooperativa e torna a casa",
"Quick search: find a host and join their game.": "Ricerca rapida: trova un ospitante e unisciti.",
"Quick search: find a world to invade.": "Ricerca rapida: trova un mondo da invadere.",
"Stop calling other players.": "Smetti di chiamare altri giocatori.",
"Stop looking for a host.": "Smetti di cercare un ospitante.",
"Stop looking for a world to invade.": "Smetti di cercare un mondo da invadere.",
"Find a host and join their game?": "Cercare un ospitante e unirsi a lui?",
"Find a world to invade?": "Trovare un mondo da invadere?",
"Need help? Ring the Hunt Bell.": "Serve aiuto? Suona la Campana della caccia.",
"Call other players into your game\\nwith the Hunt Bell.": "Chiama altri giocatori nella tua partita\\ncon la Campana della caccia.",
"Join another player's game\\nwith the Kin Bell.": "Unisciti a un altro giocatore\\ncon la Campana fraterna.",
"You caught up with <?hostName?>.": "Hai raggiunto <?hostName?>.",
"<?joinName?> caught up with you.": "<?joinName?> ti ha raggiunto.",
"You are in the host's Hunter's Dream.": "Sei nel Sogno del cacciatore dell'ospitante.",
"Hunt Bell": "Campana della caccia",
"Ring to call other players into your game.\\nCosts no Insight.": "Suonala per chiamare altri giocatori nella partita.\\nNon consuma Intuizione.",
"An old bell carried by the hunters of the\\nworkshop, its toll heard across every world.\\n\\nRing it, and hunters who answer with a\\nKin Bell may join your hunt. Together you\\nwalk the night, through every lamp,\\ndeath and dream, until the hunt is done.": "Antica campana dei cacciatori della bottega,\\nil cui rintocco si ode in ogni mondo.\\n\\nSuonala, e i cacciatori che rispondono con\\nuna Campana fraterna si uniranno alla tua\\ncaccia. Insieme attraverserete la notte,\\nogni lanterna, morte e sogno, fino alla fine.",
"Kin Bell": "Campana fraterna",
"Ring to join another players game.\\nCosts no Insight.": "Suonala per unirti alla partita di un altro.\\nNon consuma Intuizione.",
"A small bell of the choir, its chime\\nfaint, but true.\\n\\nHunters who hear a Hunt Bell may answer\\nwith it, crossing into another world to\\nstand at the side of kin. What is slain\\nthere is slain for both.": "Piccola campana del Coro, dal rintocco\\nflebile, ma sincero.\\n\\nChi ode una Campana della caccia può\\nrispondere con essa, varcando un altro\\nmondo per stare al fianco dei fratelli.\\nCiò che là viene ucciso, muore per entrambi.",
"Ring to invade another players world.\\nCosts no Insight.": "Suonala per invadere il mondo di un altro.\\nNon consuma Intuizione.",
"A bell that sounds only for the blood-drunk.\\n\\nNo maiden need ring for its toll to be\\nheard: every hunter of the night is prey,\\nalone or in company, wherever they walk.\\n\\nThe hunt for hunters never ends.": "Campana che suona solo per gli ebbri di sangue.\\n\\nNessuna fanciulla deve suonarla perché il\\nsuo rintocco sia udito: ogni cacciatore\\nnella notte è una preda, solo o in compagnia,\\novunque egli cammini.\\n\\nLa caccia ai cacciatori non ha mai fine.",
"Use to end co-op.\\nGuests go home; the host sends everyone home.": "Usala per terminare la cooperativa.\\nOspite: torni a casa. Ospitante: tutti a casa.",
"A blank of silent powder.\\n\\nIts inaudible burst breaks the resonance\\nbinding hunters of different worlds, and\\neach returns to their own night.": "Cartuccia di polvere silenziosa.\\n\\nIl suo scoppio impercettibile spezza la\\nrisonanza che lega cacciatori di mondi\\ndiversi, e ognuno torna alla propria notte.",
"No longer used. Use the Hunt Bell.": "Non più in uso. Usa la Campana della caccia.",
"No longer used. Use the Kin Bell.": "Non più in uso. Usa la Campana fraterna.",
"No longer used. Use the Bloodlust Bell.": "Non più in uso. Usa la Campana sanguinaria.",
"No longer used. Use the Parting Blank.": "Non più in uso. Usa la Cartuccia del commiato.",
"Duel Bell": "Campana del duello",
"Ring in co-op to turn on PvP between players.\\nRing again to turn it off.": "Suonala in cooperativa per attivare il PvP.\\nSuonala di nuovo per disattivarlo.",
"A cracked bell with a sour, discordant chime.\\n\\nRing it while hunting together, and the\\nresonance binding the hunters turns to\\nrivalry: they may strike one another as foes.\\n\\nRing it again to restore the old bond.\\nOnly the beckoner decides; a cooperator\\nwho rings it merely asks.": "Campana incrinata dal rintocco aspro e stonato.\\n\\nSuonala durante una caccia comune, e la\\nrisonanza che lega i cacciatori si muta in\\nrivalità: potranno colpirsi come nemici.\\n\\nSuonala di nuovo per ripristinare il legame.\\nSolo l'ospitante decide; un cooperatore\\nche la suona si limita a chiederlo.",
"You respawned at the host's lamp.": "Sei rinato alla lanterna dell'ospitante.",
"You respawned next to the host.": "Sei rinato accanto all'ospitante.",
"You respawned at the lamp.": "Sei rinato alla lanterna.",
"You respawned.": "Sei rinato.",
"The host died.\\nThey will respawn at the lamp.": "L'ospitante è morto.\\nRinascerà alla lanterna.",
"Boss reward added to your items.": "Ricompensa del boss aggiunta agli oggetti.",
"Shared progress saved:\\nthe host's progress is now in your world.": "Progressi condivisi salvati:\\nquelli dell'ospitante ora sono nel tuo mondo.",
"Shared progress saved.\\nRest at a lamp to see all of it.": "Progressi condivisi salvati.\\nRiposa a una lanterna per vederli tutti.",
"BBSC items added:": "Oggetti BBSC aggiunti:",
"PvP is ON.\\nPlayers can hurt each other.": "PvP ATTIVO.\\nI giocatori possono ferirsi a vicenda.",
"PvP is OFF.": "PvP DISATTIVATO.",
"PvP needs another player in your game.": "Il PvP richiede un altro giocatore in partita.",
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Il tuo mondo è stato ripristinato\\n(il gioco si era chiuso mentre eri ospite).\\nRiposa a una lanterna o ricarica per vederlo.",
"An item was added to your items.": "Un oggetto è stato aggiunto agli oggetti.",
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Stai osservando il cacciatore {n} di {count}.\\nTab / R1: cacciatore successivo.\\nF: telecamera libera.",
"Watching the living hunter.\\nF: free camera.": "Stai osservando il cacciatore ancora in vita.\\nF: telecamera libera.",
"Watching hunter {n} of {count}.": "Stai osservando il cacciatore {n} di {count}."
}
+87
View File
@@ -0,0 +1,87 @@
{
"<?joinName?> joined your game.": "<?joinName?> entrou no seu jogo.",
"You joined <?hostName?>'s game.": "Você entrou no jogo de <?hostName?>.",
"You died.": "Você morreu.",
"The host died.\\nYou stay in their game.": "O anfitrião morreu.\\nVocê continua no jogo dele.",
"<?deadName?> died.\\nThey will respawn.": "<?deadName?> morreu.\\nVai renascer.",
"The host <?hostName?> died.\\nYou stay in their game.": "O anfitrião <?hostName?> morreu.\\nVocê continua no jogo dele.",
"The host started the battle against\\n{boss}.": "O anfitrião iniciou a batalha contra\\n{boss}.",
"The host started a boss battle.": "O anfitrião iniciou uma batalha contra chefe.",
"{boss} defeated.": "{boss} foi derrotado.",
"Boss defeated.": "Chefe derrotado.",
"Invader <?joinName?> entered your world!": "O invasor <?joinName?> entrou no seu mundo!",
"A bell-ringing woman appeared.\\nAn invader may come.": "Uma mulher do sino apareceu.\\nUm invasor pode chegar.",
"Invader <?deadName?> was killed.": "O invasor <?deadName?> foi morto.",
"Invader <?leaveName?> left\\nand went back to their world.": "O invasor <?leaveName?> saiu\\ne voltou ao próprio mundo.",
"You invaded <?hostName?>'s world.": "Você invadiu o mundo de <?hostName?>.",
"A player answered your bell.\\nJoining...": "Um jogador respondeu ao seu sino.\\nEntrando...",
"Found a host.\\nJoining their game...": "Anfitrião encontrado.\\nEntrando no jogo dele...",
"Ring the Hunt Bell\\nto call other players?": "Tocar o Sino da Caçada\\npara chamar outros jogadores?",
"Stop the Hunt Bell": "Parar o Sino da Caçada",
"Stop the Kin Bell": "Parar o Sino Fraterno",
"Stop the Bloodlust Bell": "Parar o Sino Sanguinário",
"Hunt Bell rung.\\nWaiting for players to join...": "Sino da Caçada tocado.\\nAguardando outros jogadores...",
"Kin Bell rung.\\nLooking for a host to join...": "Sino Fraterno tocado.\\nProcurando um anfitrião...",
"Bloodlust Bell rung.\\nLooking for a world to invade...": "Sino Sanguinário tocado.\\nProcurando um mundo para invadir...",
"Nobody found.\\nRing the bell again to keep looking.": "Ninguém encontrado.\\nToque o sino de novo para continuar.",
"Someone is about to invade your world.": "Alguém está prestes a invadir seu mundo.",
"Found a world to invade.\\nEntering...": "Mundo para invadir encontrado.\\nEntrando...",
"Hunt Bell rung (password).\\nWaiting for players to join...": "Sino da Caçada tocado (senha).\\nAguardando outros jogadores...",
"Kin Bell rung (password).\\nLooking for a host to join...": "Sino Fraterno tocado (senha).\\nProcurando um anfitrião...",
"Found a host,\\nbut the beckoner is an Executioner, and thus an adversary.": "Anfitrião encontrado,\\nmas ele é um Executor e, portanto, um adversário.",
"Found a host,\\nbut the beckoner is a Vileblood, and thus an adversary.": "Anfitrião encontrado,\\nmas ele é um Sangue Vil e, portanto, um adversário.",
"Found a host,\\nbut the beckoner is a blood-addled hunter, and thus an adversary.": "Anfitrião encontrado,\\nmas ele é um caçador embriagado de sangue e, portanto, um adversário.",
"Ring the Hunt Bell to call\\nother players into your game.": "Toque o Sino da Caçada para chamar\\noutros jogadores ao seu jogo.",
"Use the Parting Blank to end co-op.": "Use o Cartucho de Despedida para encerrar o cooperativo.",
"Bloodlust Bell": "Sino Sanguinário",
"Parting Blank": "Cartucho de Despedida",
"End co-op": "Encerrar cooperativo",
"End co-op and go home": "Encerrar cooperativo e voltar",
"Quick search: find a host and join their game.": "Busca rápida: ache um anfitrião e entre no jogo dele.",
"Quick search: find a world to invade.": "Busca rápida: encontre um mundo para invadir.",
"Stop calling other players.": "Parar de chamar outros jogadores.",
"Stop looking for a host.": "Parar de procurar um anfitrião.",
"Stop looking for a world to invade.": "Parar de procurar um mundo para invadir.",
"Find a host and join their game?": "Encontrar um anfitrião e entrar no jogo dele?",
"Find a world to invade?": "Encontrar um mundo para invadir?",
"Need help? Ring the Hunt Bell.": "Precisa de ajuda? Toque o Sino da Caçada.",
"Call other players into your game\\nwith the Hunt Bell.": "Chame outros jogadores ao seu jogo\\ncom o Sino da Caçada.",
"Join another player's game\\nwith the Kin Bell.": "Entre no jogo de outro jogador\\ncom o Sino Fraterno.",
"You caught up with <?hostName?>.": "Você alcançou <?hostName?>.",
"<?joinName?> caught up with you.": "<?joinName?> alcançou você.",
"You are in the host's Hunter's Dream.": "Você está no Sonho do Caçador do anfitrião.",
"Hunt Bell": "Sino da Caçada",
"Ring to call other players into your game.\\nCosts no Insight.": "Toque para chamar outros jogadores ao seu jogo.\\nNão consome Discernimento.",
"An old bell carried by the hunters of the\\nworkshop, its toll heard across every world.\\n\\nRing it, and hunters who answer with a\\nKin Bell may join your hunt. Together you\\nwalk the night, through every lamp,\\ndeath and dream, until the hunt is done.": "Um velho sino levado pelos caçadores da\\noficina, cujo dobre ecoa em todos os mundos.\\n\\nToque-o, e caçadores que responderem com um\\nSino Fraterno poderão se unir à sua caçada.\\nJuntos, vocês percorrem a noite, por lampião,\\nmorte e sonho, até que a caçada termine.",
"Kin Bell": "Sino Fraterno",
"Ring to join another players game.\\nCosts no Insight.": "Toque para entrar no jogo de outro jogador.\\nNão consome Discernimento.",
"A small bell of the choir, its chime\\nfaint, but true.\\n\\nHunters who hear a Hunt Bell may answer\\nwith it, crossing into another world to\\nstand at the side of kin. What is slain\\nthere is slain for both.": "Um pequeno sino do coro, de toque\\nfraco, porém verdadeiro.\\n\\nCaçadores que ouvem um Sino da Caçada podem\\nrespondê-lo, cruzando para outro mundo para\\nficar ao lado dos seus. O que ali é abatido\\né abatido para ambos.",
"Ring to invade another players world.\\nCosts no Insight.": "Toque para invadir o mundo de outro jogador.\\nNão consome Discernimento.",
"A bell that sounds only for the blood-drunk.\\n\\nNo maiden need ring for its toll to be\\nheard: every hunter of the night is prey,\\nalone or in company, wherever they walk.\\n\\nThe hunt for hunters never ends.": "Um sino que soa apenas para os ébrios de sangue.\\n\\nNenhuma donzela precisa tocá-lo para que seu\\ndobre seja ouvido: todo caçador da noite é presa,\\nsozinho ou acompanhado, onde quer que ande.\\n\\nA caçada aos caçadores nunca termina.",
"Use to end co-op.\\nGuests go home; the host sends everyone home.": "Use para encerrar o cooperativo.\\nConvidados voltam; o anfitrião devolve todos.",
"A blank of silent powder.\\n\\nIts inaudible burst breaks the resonance\\nbinding hunters of different worlds, and\\neach returns to their own night.": "Um cartucho de pólvora silenciosa.\\n\\nSua explosão inaudível rompe a ressonância\\nque une caçadores de mundos diferentes, e\\ncada um retorna à sua própria noite.",
"No longer used. Use the Hunt Bell.": "Fora de uso. Use o Sino da Caçada.",
"No longer used. Use the Kin Bell.": "Fora de uso. Use o Sino Fraterno.",
"No longer used. Use the Bloodlust Bell.": "Fora de uso. Use o Sino Sanguinário.",
"No longer used. Use the Parting Blank.": "Fora de uso. Use o Cartucho de Despedida.",
"Duel Bell": "Sino do Duelo",
"Ring in co-op to turn on PvP between players.\\nRing again to turn it off.": "No cooperativo, toque para ativar o PvP.\\nToque de novo para desativá-lo.",
"A cracked bell with a sour, discordant chime.\\n\\nRing it while hunting together, and the\\nresonance binding the hunters turns to\\nrivalry: they may strike one another as foes.\\n\\nRing it again to restore the old bond.\\nOnly the beckoner decides; a cooperator\\nwho rings it merely asks.": "Um sino rachado, de toque azedo e dissonante.\\n\\nToque-o durante uma caçada conjunta, e a\\nressonância que une os caçadores vira\\nrivalidade: podem golpear uns aos outros.\\n\\nToque-o de novo para restaurar o antigo laço.\\nSó o anfitrião decide; o cooperador\\nque o toca apenas pede.",
"You respawned at the host's lamp.": "Você renasceu no lampião do anfitrião.",
"You respawned next to the host.": "Você renasceu ao lado do anfitrião.",
"You respawned at the lamp.": "Você renasceu no lampião.",
"You respawned.": "Você renasceu.",
"The host died.\\nThey will respawn at the lamp.": "O anfitrião morreu.\\nEle vai renascer no lampião.",
"Boss reward added to your items.": "Recompensa do chefe adicionada aos seus itens.",
"Shared progress saved:\\nthe host's progress is now in your world.": "Progresso compartilhado salvo:\\no progresso do anfitrião agora está no seu mundo.",
"Shared progress saved.\\nRest at a lamp to see all of it.": "Progresso compartilhado salvo.\\nDescanse num lampião para ver tudo.",
"BBSC items added:": "Itens do BBSC adicionados:",
"PvP is ON.\\nPlayers can hurt each other.": "PvP ATIVADO.\\nOs jogadores podem se ferir.",
"PvP is OFF.": "PvP DESATIVADO.",
"PvP needs another player in your game.": "O PvP precisa de outro jogador no seu jogo.",
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Seu próprio mundo foi restaurado\\n(o jogo fechou enquanto você era convidado).\\nDescanse num lampião ou recarregue para vê-lo.",
"An item was added to your items.": "Um item foi adicionado aos seus itens.",
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Observando o caçador {n} de {count}.\\nTab / R1: próximo caçador.\\nF: câmera livre.",
"Watching the living hunter.\\nF: free camera.": "Observando o caçador vivo.\\nF: câmera livre.",
"Watching hunter {n} of {count}.": "Observando o caçador {n} de {count}."
}
+87
View File
@@ -0,0 +1,87 @@
{
"<?joinName?> joined your game.": "Игрок <?joinName?> вошёл в вашу игру.",
"You joined <?hostName?>'s game.": "Вы вошли в игру игрока <?hostName?>.",
"You died.": "Вы погибли.",
"The host died.\\nYou stay in their game.": "Хозяин мира погиб.\\nВы остаётесь в его игре.",
"<?deadName?> died.\\nThey will respawn.": "Игрок <?deadName?> погиб.\\nОн возродится.",
"The host <?hostName?> died.\\nYou stay in their game.": "Хозяин мира <?hostName?> погиб.\\nВы остаётесь в его игре.",
"The host started the battle against\\n{boss}.": "Хозяин мира вступил в бой:\\n{boss}.",
"The host started a boss battle.": "Хозяин мира начал битву с боссом.",
"{boss} defeated.": "Босс повержен: {boss}.",
"Boss defeated.": "Босс повержен.",
"Invader <?joinName?> entered your world!": "Захватчик <?joinName?> вторгся в ваш мир!",
"A bell-ringing woman appeared.\\nAn invader may come.": "Появилась женщина с колоколом.\\nМожет прийти захватчик.",
"Invader <?deadName?> was killed.": "Захватчик <?deadName?> убит.",
"Invader <?leaveName?> left\\nand went back to their world.": "Захватчик <?leaveName?> ушёл\\nи вернулся в свой мир.",
"You invaded <?hostName?>'s world.": "Вы вторглись в мир игрока <?hostName?>.",
"A player answered your bell.\\nJoining...": "Игрок откликнулся на ваш колокол.\\nПодключение...",
"Found a host.\\nJoining their game...": "Хозяин мира найден.\\nПодключение к его игре...",
"Ring the Hunt Bell\\nto call other players?": "Позвонить в Колокол охоты,\\nчтобы позвать других игроков?",
"Stop the Hunt Bell": "Заглушить Колокол охоты",
"Stop the Kin Bell": "Заглушить Колокол родства",
"Stop the Bloodlust Bell": "Заглушить Колокол кровожадности",
"Hunt Bell rung.\\nWaiting for players to join...": "Колокол охоты звонит.\\nОжидание игроков...",
"Kin Bell rung.\\nLooking for a host to join...": "Колокол родства звонит.\\nПоиск хозяина мира...",
"Bloodlust Bell rung.\\nLooking for a world to invade...": "Колокол кровожадности звонит.\\nПоиск мира для вторжения...",
"Nobody found.\\nRing the bell again to keep looking.": "Никого не найдено.\\nПозвоните снова, чтобы искать дальше.",
"Someone is about to invade your world.": "Кто-то готовится вторгнуться в ваш мир.",
"Found a world to invade.\\nEntering...": "Мир для вторжения найден.\\nВторжение...",
"Hunt Bell rung (password).\\nWaiting for players to join...": "Колокол охоты звонит (пароль).\\nОжидание игроков...",
"Kin Bell rung (password).\\nLooking for a host to join...": "Колокол родства звонит (пароль).\\nПоиск хозяина мира...",
"Found a host,\\nbut the beckoner is an Executioner, and thus an adversary.": "Хозяин мира найден,\\nно призвавший — Палач, а значит, противник.",
"Found a host,\\nbut the beckoner is a Vileblood, and thus an adversary.": "Хозяин мира найден,\\nно призвавший — Нечистокровный, а значит, противник.",
"Found a host,\\nbut the beckoner is a blood-addled hunter, and thus an adversary.": "Хозяин мира найден,\\nно призвавший — одурманенный кровью охотник, а значит, противник.",
"Ring the Hunt Bell to call\\nother players into your game.": "Позвоните в Колокол охоты, чтобы\\nпризвать других игроков в свою игру.",
"Use the Parting Blank to end co-op.": "Совместную игру завершает Прощальный патрон.",
"Bloodlust Bell": "Колокол кровожадности",
"Parting Blank": "Прощальный патрон",
"End co-op": "Завершить совместную игру",
"End co-op and go home": "Завершить и вернуться домой",
"Quick search: find a host and join their game.": "Быстрый поиск: найти хозяина мира и войти в его игру.",
"Quick search: find a world to invade.": "Быстрый поиск: найти мир для вторжения.",
"Stop calling other players.": "Перестать звать других игроков.",
"Stop looking for a host.": "Прекратить поиск хозяина мира.",
"Stop looking for a world to invade.": "Прекратить поиск мира для вторжения.",
"Find a host and join their game?": "Найти хозяина мира и войти в его игру?",
"Find a world to invade?": "Найти мир для вторжения?",
"Need help? Ring the Hunt Bell.": "Нужна помощь? Позвоните в Колокол охоты.",
"Call other players into your game\\nwith the Hunt Bell.": "Зовите других игроков в свою игру\\nс помощью Колокола охоты.",
"Join another player's game\\nwith the Kin Bell.": "Входите в игру других игроков\\nс помощью Колокола родства.",
"You caught up with <?hostName?>.": "Вы догнали игрока <?hostName?>.",
"<?joinName?> caught up with you.": "Игрок <?joinName?> догнал вас.",
"You are in the host's Hunter's Dream.": "Вы во Сне охотника хозяина мира.",
"Hunt Bell": "Колокол охоты",
"Ring to call other players into your game.\\nCosts no Insight.": "Призывает других игроков в вашу игру.\\nНе расходует Озарение.",
"An old bell carried by the hunters of the\\nworkshop, its toll heard across every world.\\n\\nRing it, and hunters who answer with a\\nKin Bell may join your hunt. Together you\\nwalk the night, through every lamp,\\ndeath and dream, until the hunt is done.": "Старый колокол охотников мастерской;\\nего звон слышен во всех мирах.\\n\\nПозвоните в него, и охотники, что ответят\\nКолоколом родства, разделят вашу охоту.\\nВместе вы пройдёте ночь — через каждый\\nфонарь, смерть и сон, пока охота не кончится.",
"Kin Bell": "Колокол родства",
"Ring to join another players game.\\nCosts no Insight.": "Позволяет войти в игру другого игрока.\\nНе расходует Озарение.",
"A small bell of the choir, its chime\\nfaint, but true.\\n\\nHunters who hear a Hunt Bell may answer\\nwith it, crossing into another world to\\nstand at the side of kin. What is slain\\nthere is slain for both.": "Малый колокол хора, чей звон\\nслаб, но верен.\\n\\nУслышав Колокол охоты, охотник может\\nответить им и перейти в иной мир, чтобы\\nвстать плечом к плечу с родными. Всё, что\\nпало там, пало для обоих.",
"Ring to invade another players world.\\nCosts no Insight.": "Позволяет вторгнуться в мир другого игрока.\\nНе расходует Озарение.",
"A bell that sounds only for the blood-drunk.\\n\\nNo maiden need ring for its toll to be\\nheard: every hunter of the night is prey,\\nalone or in company, wherever they walk.\\n\\nThe hunt for hunters never ends.": "Колокол, звучащий лишь для опьянённых кровью.\\n\\nЧтобы его звон услышали, не нужна дева\\nс колоколом: любой охотник ночи — добыча,\\nодин или в компании, где бы он ни был.\\n\\nОхота на охотников не кончается никогда.",
"Use to end co-op.\\nGuests go home; the host sends everyone home.": "Завершает совместную игру.\\nГость уходит домой, хозяин — отправляет всех.",
"A blank of silent powder.\\n\\nIts inaudible burst breaks the resonance\\nbinding hunters of different worlds, and\\neach returns to their own night.": "Патрон с беззвучным порохом.\\n\\nЕго неслышный выстрел рвёт резонанс,\\nсвязующий охотников разных миров, и\\nкаждый возвращается в свою ночь.",
"No longer used. Use the Hunt Bell.": "Устарел. Используйте Колокол охоты.",
"No longer used. Use the Kin Bell.": "Устарел. Используйте Колокол родства.",
"No longer used. Use the Bloodlust Bell.": "Устарел. Используйте Колокол кровожадности.",
"No longer used. Use the Parting Blank.": "Устарел. Используйте Прощальный патрон.",
"Duel Bell": "Колокол дуэли",
"Ring in co-op to turn on PvP between players.\\nRing again to turn it off.": "В совместной игре включает PvP между игроками.\\nПовторный звон выключает его.",
"A cracked bell with a sour, discordant chime.\\n\\nRing it while hunting together, and the\\nresonance binding the hunters turns to\\nrivalry: they may strike one another as foes.\\n\\nRing it again to restore the old bond.\\nOnly the beckoner decides; a cooperator\\nwho rings it merely asks.": "Треснувший колокол с резким, нестройным звоном.\\n\\nЕсли позвонить в него на совместной охоте,\\nрезонанс, что связывает охотников, обратится\\nсоперничеством: они смогут разить друг друга.\\n\\nПозвоните вновь, чтобы вернуть прежние узы.\\nРешает лишь призвавший; союзник,\\nпозвонив в него, лишь просит об этом.",
"You respawned at the host's lamp.": "Вы возродились у фонаря хозяина мира.",
"You respawned next to the host.": "Вы возродились рядом с хозяином мира.",
"You respawned at the lamp.": "Вы возродились у фонаря.",
"You respawned.": "Вы возродились.",
"The host died.\\nThey will respawn at the lamp.": "Хозяин мира погиб.\\nОн возродится у фонаря.",
"Boss reward added to your items.": "Награда за босса добавлена в инвентарь.",
"Shared progress saved:\\nthe host's progress is now in your world.": "Общий прогресс сохранён:\\nпрогресс хозяина теперь в вашем мире.",
"Shared progress saved.\\nRest at a lamp to see all of it.": "Общий прогресс сохранён.\\nОтдохните у фонаря, чтобы увидеть всё.",
"BBSC items added:": "Добавлены предметы BBSC:",
"PvP is ON.\\nPlayers can hurt each other.": "PvP ВКЛЮЧЕНО.\\nИгроки могут ранить друг друга.",
"PvP is OFF.": "PvP ВЫКЛЮЧЕНО.",
"PvP needs another player in your game.": "Для PvP нужен ещё один игрок в вашей игре.",
"Your own world was put back\\n(the game had closed while you were a guest).\\nRest at a lamp or reload to see it.": "Ваш собственный мир восстановлен\\n(игра закрылась, пока вы были гостем).\\nОтдохните у фонаря или перезапустите игру.",
"An item was added to your items.": "Предмет добавлен в инвентарь.",
"Watching hunter {n} of {count}.\\nTab / R1: next hunter.\\nF: free camera.": "Вы наблюдаете за охотником {n} из {count}.\\nTab / R1: следующий охотник.\\nF: свободная камера.",
"Watching the living hunter.\\nF: free camera.": "Вы наблюдаете за живым охотником.\\nF: свободная камера.",
"Watching hunter {n} of {count}.": "Вы наблюдаете за охотником {n} из {count}."
}
File renamed without changes.
@@ -7,7 +7,7 @@
#include <ranges>
#include <string_view>
#include "assert.h"
#include "common/assert.h"
namespace Common {
File renamed without changes.
@@ -9,10 +9,10 @@
#include <vector>
#include <map>
#include "assert.h"
#include "bit_field.h"
#include "singleton.h"
#include "types.h"
#include "common/assert.h"
#include "common/bit_field.h"
#include "common/singleton.h"
#include "common/types.h"
namespace Core {
class Emulator;
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport (Windows): small POSIX helpers the renderer uses for diagnostics and thread
// identity. Force-included into the GPU library on Windows only (CMakeLists.txt); kept free
// of <windows.h> so its macros do not reach every translation unit.
#pragma once
#ifdef _WIN32
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllimport) unsigned long __stdcall GetCurrentThreadId(void);
static inline int gettid(void) { return (int)GetCurrentThreadId(); }
#ifdef __cplusplus
}
#endif
#endif
Loaded 100 of 1427 files, more files were not shown because too many files have changed in this diff. Show more