Compare commits

...
84 Commits
Author SHA1 Message Date
ayozetr 7135aa9288 0.51-linux.4
The launcher's version for the About page's updates.
2026-10-13 10:40:20 +01:00
ayozetr 48de7625b5 AppImage: Mesa's Vulkan drivers for AMD and Intel again
quick-sharun collects Mesa's Vulkan drivers from the build host, and the host
had NVIDIA's driver only: no AMD or Intel driver went into the package, and
the game found no GPU on those cards ("Failed to enumerate physical devices:
ErrorInitializationFailed", #4), in every release built that way. The script
now stops when vulkan-radeon or vulkan-intel is missing on the host or from
the AppDir. sharun points Vulkan at the packaged drivers, plus the host's
NVIDIA driver when there is one.

Thanks: nr1971, who reported it.
2026-10-13 10:40:20 +01:00
ayozetr 1ca5a39b3a Launcher: a Thanks page in About
"None of this would be possible without them": everyone whose work is in
this fork, with what it brought, and those who helped it with reports and
tests. Each name opens its GitHub profile; the people credited on Nexus Mods
and the game patches' authors are listed after them. Someone whose work was
only looked at is not listed: whoever a commit brings something from is
added in that commit. In every launcher language.
2026-10-13 01:49:20 +01:00
ayozetr 34ca9d1340 0.51-linux.3
The launcher's version for the About page's updates.
2026-10-13 01:26:23 +01:00
ayozetr b18055557b On-screen keyboard: the letters of the menu's language
The keyboard had the English letters only, so a controller could not type
the ñ that a physical keyboard gives the name. A row with the menu
language's own letters now sits above the actions: Spanish ñ, the accented
vowels, ü, ¿ and ¡; the accents of Italian, French and Portuguese, ç among
them; German's umlauts and ß. English, Russian and Chinese keep four rows.
2026-10-13 01:22:04 +01:00
ayozetr ebb79d3774 Loading and smoothness: three game patches from bbhost
A new group in the launcher's Settings page, with patches from
droogie/bbhost (patches/*.toml), each checked against the 1.09
executable's original bytes, noted beside it:

- Quick loading after death (on): a load after a death or lamp travel
  ends when the area is ready, 2-3 s, not after the 12-second minimum
  the in-game step waits out. The multiplayer wait stays the game's.
- No stutter every ~50 s (on): the player's recording is deflated on the
  main thread at level 9, ~23 ms in one frame; level 1 does it in a
  fraction of that, and inflate reads either.
- Third resource preparation thread (off, experimental): the resource
  system's third post-processor keeps running in play.

The first two are on also when bbport.ini does not name them yet.
2026-10-13 01:22:04 +01:00
ayozetr 479e964d90 Overlay: a notice while shaders compile
"Compiling shaders…" in the bottom-left corner while the pipeline cache
compiles a shader or pipeline, kept 400 ms after the last one and faded
out over 400 ms more, so a burst does not flicker. A stutter at that
moment is the shader cache filling, once per place, not a fault worth
reporting. On by default; "Notice while shaders compile" in the menu's
Other section turns it off (shader_notice in bbport.ini).

From SrShadowy/bloodborne_pc (gpu/shim/bbport_shader_tracker), counted
in the existing CompileTimer.
2026-10-13 01:22:00 +01:00
ayozetr fb25a78b5d On-screen keyboard for the character name, typed with a controller
The game's text dialog (ImeDialog: the character's name) could only be
typed on a physical keyboard, so in the Steam Deck's Game Mode or with a
controller on the sofa a character could not 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.

From SrShadowy/bloodborne_pc (gpu/shim/bbport_keyboard.cpp), reworked:
the keyboard is drawn in the existing dialog and its edits are queued
to the window thread, which owns the text and the window. There, the
keyboard's mouse clicks changed the text and the window title from the
render thread. Its texts are in the menu's languages.
2026-10-13 01:22:00 +01:00
ayozetr 5bf966b85a Launcher: Test the connection signs in with the account too
It only checked that the servers answered, so a wrong name or password
showed up in the game as "not signed in to PlayStation Network". With
the account filled in, the test now signs in to the shadNet server as
the game does (ServerInfo, then a Login request) and leaves at once,
and says what the server answered: signed in, wrong name or password,
no such account, or already signed in (the game running with the same
account). srv.shadps4.net answers "wrong password" for an unknown name
as well, so the text names both.

Tests against a stand-in server check the request and each answer.
ONLINE.md says so, and that the game now keeps trying when the server
did not answer at start.
2026-10-13 01:07:23 +01:00
ayozetr e7f5b9f575 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,
Tailscale 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 dropped
connection was retried only by the worker a first successful sign-in
started. An unreachable server now schedules the same background retries,
with a backoff of up to 30 s, and the game signs in once it answers. A
sign-in that fails because the server did not answer is retried too.

Each user's latest attempt keeps how it ended. A server that answers and
refuses (wrong name or password, an incompatible protocol) is not retried,
at start or after a drop, since retrying cannot help.
2026-10-13 01:07:23 +01:00
Supermedo 51927fb018 SRT walker: leave speculative walks to their recovery point
Draw preparation workers and the recording thread's table check walk resource
tables ahead of the game and may read memory it has reused. Their faults went
to the SRT walker handler, which patched the walker for good: one stale read
made that load zero for the rest of the session. They now go back to their
recovery point, and the GPU thread walks the tables itself.

The patch is also written in an order that keeps every intermediate state
harmless for threads running that code: the nops from the end, then the ModRM
byte, the REX prefix and the xor opcode last, with atomic stores.

From Supermedo/bloodborne_pc 479f2f5, on top of 0xCydral's lock and retry.
2026-10-13 00:57:32 +01:00
0xCydral c5c983c248 fix(gpu): draw preparation steps over an invalid PM4 header like the GPU thread
The GPU thread steps over an invalid packet header (a type 0 header
Bloodborne leaves in a command buffer at an area load) instead of ending
the game, but the draw preparation scanner still stopped reading that
buffer there. Its register state then differed from the GPU thread's for
the rest of the session, so no prepared draw was used again and every
new pipeline was compiled on the GPU thread: on an RX 6700 XT draw
preparation fell from about 96% used to 0% after one such header, with
stutter. Both now skip by one rule (pm4_resync.h); pm4-resync-test
checks it on the bytes from that log.

From 0xCydral's ed19a84
2026-10-13 00:57:32 +01:00
0xCydral a80e1ed7d5 fix(shader): an exception in speculative draw preparation ends the attempt, not the game
Draw-preparation workers specialize shaders from game memory the game may
still be writing. A fault there already ends the attempt and leaves the
draw to the GPU thread; 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. One cold start in fourteen died that way
(exception 0x20474343, exit 3) and could not be reproduced. The
speculative specialization now treats an exception like a fault.
siglongjmp restores registers without unwinding, so a fault jump out of
the new try block is unaffected.

From 0xCydral's df619c7 (adapted: here only the specialization runs
ahead of the GPU thread)
2026-10-13 00:57:32 +01:00
0xCydral a07910eb89 fix: a failed check in speculative draw preparation abandons it instead of stopping the game
Draw-preparation workers specialize shaders ahead of the GPU thread from
descriptors the game may still be rewriting. Faults there already
returned to the worker's recovery point; a failed check stopped the port
with exit 23. It now leaves through the same recovery point and the GPU
thread prepares the draw in order. The SRT walker fault handler disarms
the recovery point while it holds its lock.

From 0xCydral's 917d2c0 (adapted: here the workers specialize programs
already translated, they do not translate new ones)
2026-10-13 00:57:32 +01:00
0xCydral 2e4a55f8bb fix(gpu): 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. The pipeline cache checks that a stage's 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 b99535b, itself from yumlevi/bloodborne_pc ce385b6 and
GoncaloLobo0/bloodborne_pc_win ef70ed2. Its other half, hashing textures
through the backing view, is left out: here a read-protected page holds
GPU-written data the fault reads back, and the backing view would hash
the stale copy.
2026-10-13 00:57:32 +01:00
0xCydral f1d897ea0d fix(shader): SRT walker fault handler is safe for concurrent draw-prep workers
Two draw-preparation workers can fault on the same SRT-walker store. The
second one decoded the already-patched instruction and tripped the MOV
ASSERT (exit 23). The handler now takes a mutex and returns early when the
instruction is already the xor patch. Registration of the handler goes
through call_once, and walkers loaded through RegisterWalkerCode register it
too: when every walker came from the pipeline cache, it was never
registered.

From Mrsuss60/bloodborne_pc_windows_port b7001bc (fault handler hunk only,
merged by hand)
2026-10-13 00:57:32 +01:00
0xCydral 9779cbbc70 fix(fsr4): depth barriers name the depth and stencil aspects
The vendored FSR 4 backend issued its layout barriers on every external
image with VK_IMAGE_ASPECT_COLOR_BIT, invalid for the D32_SFLOAT_S8_UINT
depth image (VUID-VkImageMemoryBarrier-image-03319 and
subresourceRange-09601, 20 each in a validation run).

New fsr-vulkan patch 0002 adds an optional aspectMask to
FfxFsr4VkExternalImageState (0 = color); Register passes DEPTH|STENCIL for
the depth image.
2026-10-13 00:57:32 +01:00
andrecabralgit e36717374e 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-10-13 00:48:43 +01:00
Jgamrot 542e9ada19 Menu: checkboxes and sliders keep their changes with any compiler
Store(value, v, ImGui::Checkbox(label, &v)) read v before the checkbox had
changed it on compilers that evaluate arguments left to right: C++ leaves the
order unspecified. GCC happens to call the widget first, so our builds worked;
clang lost every change. The widget now runs before Store.

From Supermedo/bloodborne_pc#19 (5c660f9).
2026-10-13 00:48:26 +01:00
ayozetr 9e629d2efd Launcher: one shadNet account for each kind of server
shadPS4's public server and a server of your own have their own accounts, but
the Online page kept a single online ID and password, so switching servers
sent one server's account to the other. The page now keeps one account for
each (online_accounts) and swaps them when the server changes. Settings from
before give their account to the server chosen then.
2026-10-12 17:43:38 +01:00
ayozetr 74584d57b5 Trophies: the platinum no longer waits for the add-on's trophies
On the PS4 the platinum needs the base game's trophies only; ours waited for
every other one, so without The Old Hunters' six (34-39) it never came. The
trophy list says which ones count: their pid names the platinum, and an
add-on's is -1. scripts/trophies.py keeps that as a sixth column of
trophies.tsv, and the runtime awards the platinum once those are earned. A
list without the column, from an older run, still needs every other trophy.
2026-10-12 13:05:43 +01:00
ayozetrandQiming-Liu 40b67fc784 Translations: native wording for the Simplified Chinese launcher
Takes Qiming-Liu's wording from deadinside28/bloodborne_pc#157 for the
21 strings where ours differed: 联机 and 账号 for online play and the
account, 「」 quotes, 检测 for the connection check, 自定义 for a custom
file, and a plain "{} / {}" for the DLL pass counter.

Co-authored-by: Qiming-Liu <[email protected]>
2026-10-12 13:01:22 +01:00
ayozetr 82923859db Launcher: swap A and B for Nintendo-style controllers
A switch under Controls binds cross to the gamepad's east button and
circle to the south one (pad.cross=b, pad.circle=a in bbport.ini), and
the Xbox and Switch prompts swap their cross and circle glyphs to match.

Closes #3
2026-10-12 10:20:59 +01:00
ayozetr 2ea5b61641 Issue templates: a bug report form, a feature request, and links to the guides
The bug report asks for the version, the system, the graphics settings, the online mode and the exported log, and that the dump is the reporter's own. Blank issues are off; the troubleshooting and online guides are linked instead.
2026-10-11 23:52:14 +01:00
ayozetr 0ca95492c4 In-game menu in Simplified Chinese
The overlay menu (Insert / L3+R3) and the trophy notice get Simplified
Chinese, the launcher's eighth language: a column in
bbport_menu_i18n.h, zh_CN in bbport.ini and BB_MENU_LANGUAGE, and
简体中文 in the menu's language list.

The menu's font has no Chinese characters. The first time the menu
opens or speaks Chinese, a system font is merged into it: BB_CJK_FONT,
else fontconfig's choice for zh-cn (Noto Sans CJK, WenQuanYi), checked
to cover zh-cn since fontconfig answers with some font regardless.
ImGui 1.92 rasterizes glyphs as they are drawn, so only the file is
loaded, not a texture of every character. Without such a font the log
says what to install.

The game's own System menu pages are drawn with the game's font, which
has no Chinese characters in the western releases: they stay in English
for Chinese.
2026-10-11 18:52:39 +01:00
ayozetr 4fd797f7e9 README: the launcher is in Simplified Chinese too 2026-10-11 18:32:18 +01:00
ayozetr 1ebe35c2e8 Translations: Simplified Chinese launcher complete, and the game menu's missing texts
The launcher's Simplified Chinese came from upstream and stopped with
the texts it had; the 87 added since (Online, Trophies, Discord, Steam,
DLC, the log export, Ko-fi) are translated now, and the test keeps it
complete like the other languages.

Nine texts of the in-game menu (Insert / L3+R3) showed in English in
Spanish, Italian, French, German and Portuguese: the DLSS, FSR 4.1.1
and FSR 4 hints, the free camera and debug menu hints, and the quit
buttons. Four of them were translated in an older wording that still
said FSR where the menu now says upscaler.

test_menu_i18n.py checks that every text of the overlay and the game
menu has a row in bbport_menu_i18n.h, with every language and the same
printf fields.
2026-10-11 18:31:06 +01:00
devilismyfriend 17e7741934 LRU cache: the early stop of ForEachItemBelow works
It compared std::invoke_result<...> (the trait, not its type) with bool,
so a callback returning true never stopped the walk: every texture cache
collection visited every old image, past its deletions and its 256-image
cap.

From yumlevi's "Texture cache: images it must keep no longer block
evictions" (d7a53a5); its other half, kept images not using up the
deletions, was already here.
2026-10-11 18:14:05 +01:00
ayozetr 97a60dd497 0.51-linux.2, and co-op on your own server tested with two players
The launcher's version for the About page's updates. docs/SERVER.md and the README say that a
private server has been tested with a Linux and a Windows player: summons, a boss fight together
and summoning again after the helper died.
2026-10-11 16:51:45 +01:00
0xCydral a3a8a3af6e fix(vulkan): include vulkan.hpp with NDEBUG so the dispatcher layout matches
vulkan.hpp adds two fields to DispatchLoaderBase when NDEBUG is undefined, which moves every
function pointer in vk::detail::DispatchLoaderDynamic by 16 bytes. libbbgpu is built with NDEBUG
while the renderer tests build with -UNDEBUG so their asserts run, so the two sides disagreed on
the layout. The linker keeps one inline copy of DispatchLoaderDynamic::init(Instance): the test
translation unit's copy read vkGetInstanceProcAddr at offset 88 while CreateInstance had stored it
at offset 72, and Instance construction called a null pointer (exit 139 on Windows).

vk_common.h now defines NDEBUG around the vulkan.hpp include and restores assert() afterwards,
so every translation unit sees the same layout while test asserts stay active.

(cherry picked from commit 03b383b34ccb74085d74baa1e3b87ca0b50013aa)
2026-10-11 16:50:59 +01:00
0xCydral f1091cc0b1 fix(gpu): 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 test route, 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; 85 px at 1706x960).

Measured on 24 frames at the same moment: object vectors over 100 px went
from 2.8% of the picture on the worst frames of two runs to none. The camera
motion test gains two cases, a far object vector that is rejected and a 30 px
one that is kept; that test does not run on Windows yet.

(cherry picked from commit a12911460872b37aba2bff4895903ad20aabf1e2)
2026-10-11 16:50:59 +01:00
0xCydral c496667284 feat(log): a performance line every 30 seconds in normal play
Players' last_run.log 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".

(cherry picked from commit 6c73bf375ea55972fd19bb5dcc254a92ab57ee54)
2026-10-11 16:50:59 +01:00
0xCydral a8b7a8eaa1 fix(gpu): keep the default indirect guard limit when BB_INDIRECT_GUARD_MAX is not a positive number
An unparsable value gave strtoul 0, so the shader skipped every indirect dispatch without a word. A value that is not a number from 1 to 4294967295 now keeps the default and prints one warning line.

(cherry picked from commit 62177377cf6c4a390d95d61b804505b077e7648c)
2026-10-11 16:50:59 +01:00
0xCydral 4d934b7529 fix(run.sh): give an explicit render size above 1080p the larger direct memory
run.sh raised BB_DMEM_MB to 9152 MiB only on the scaled path. An explicit BB_RENDER_RES skips that path, but patches.py still grows the graphics heap above 1080p, so the game asked for more memory than it had and stopped at start (exit 139). run.sh now does what run.py does since e5276cf: a WxH size above 1920x1080 by pixel count gets 9152 MiB unless BB_DMEM_MB is already set.

(cherry picked from commit 674b72e650f990dce906bc43446d024ab7109520)
2026-10-11 16:50:59 +01:00
0xCydral 442c2b66bd fix(gpu): resync the graphics command decoder at an invalid PM4 header
A cut-short SET_SH_REG leaves the next packets inside its body, and the
decoder then hit UNREACHABLE (Unimplemented PM4 type 0) at area loads. On
that path only, decoding now resumes at the next dword that can start a
type 3 packet, so the rest of the buffer and its fences still run. Each
resync logs the header dword and its offset; the buffer report stays
limited to the first 8.

From GoncaloLobo0/bloodborne_pc_win a334d26 and 9dc909d (adapted: crash path
only, no look-ahead on valid register packets)

(cherry picked from commit e0b464db61362d5c8b966ec645c592cf0a680667)
2026-10-11 16:50:59 +01:00
0xCydral d7a42fde94 fix(motion): object vectors for 16-64 KiB bone palettes; unknown SnormNz format is not fatal
Palettes of whole bones between 16 and 64 KiB (24576, 49152 bytes: the
large beasts in Central Yharnam) are now LargeSkeleton: gated like small
skeletons (history only while the palette changes). BB_MOTION_LARGE=0
restores the old rule (camera vectors only).

MapNumberConversion returns NumberConversion::None for a data format with
no SnormNz mapping instead of hitting UNREACHABLE (seen with data format 45,
a stale or unused descriptor).

From bmy/bbport-mac 80898cf and 5cf5684 (gpu part only)

(cherry picked from commit 4ff6b6292df844129332d75c6a1eb56eda14232b)
2026-10-11 16:50:59 +01:00
0xCydral d0318b1ab5 fix(upscaler): UI mask draws write the UI's output-size stencil
Scaleform masks are drawn with color writes off, so they reached the UI
composition without a color target and stayed in the game-size depth. The
next UI draw switched to the output-size UI depth, whose stencil
PrepareUiDepth clears, and the mask was gone: images cut out through a mask
(the loading screen's item picture) vanished at output sizes other than
1080p.

The frame generation HUD-less capture is unaffected: it runs in the
!Scaled() path before ui_phase, and the new branch only applies in ui_phase.

From bmy/bbport-mac e4f7d33

(cherry picked from commit 55b4a0d7247ec05dafc0d40643a2a146869c9531)
2026-10-11 16:50:59 +01:00
0xCydral 1bc4f37504 fix(texture-cache): hash the same guest range at marking and refresh
MarkAsMaybeDirty hashed the whole image but RefreshImage only its first 8x8
pixels, so the two never matched and stale guest memory replaced
GPU-written small images (black character preview, missing loading-screen
item picture). Both now use MaybeDirtyHash, and every upload records its
mip hash so an unchanged guest copy is not re-uploaded over GPU contents.

From yumlevi/bloodborne_pc 43fcf95

(cherry picked from commit 0d2dfd2f4ba3a13d5bee415769f5f0ff4f58fb2e)
2026-10-11 16:50:59 +01:00
0xCydral 44a6b7c3ce fix(shader): ReadUdReg on a null user-data table returns an empty sharp
An unbound descriptor table dereferenced a near-null pointer (access
violation at 0x8 on map load). Return a zeroed value so the existing
empty-sharp handling runs.

From Mrsuss60/bloodborne_pc_windows_port 8d8ac28 (info.h hunk only)

(cherry picked from commit 5e4513bad8b952bea95f6df76d94c97d5e827de8)
2026-10-11 16:50:59 +01:00
0xCydral efdf7b176e fix(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.

From bmy/bbport-mac 9ffc78d

(cherry picked from commit 389a1e5c3573460f73f96f255f5fbf7da43c981c)
2026-10-11 16:50:59 +01:00
ayozetr 4f7df3de5c Trophies: a notice in the game and a Trophies page
The game's trophies come from the player's own dump: scripts/trophies.py reads
sce_sys/trophy/trophy00.trp, keeps its icons, and decrypts the trophy lists with the PS4's
trophy key (shadPS4's TrophyKey, entered in the launcher), in the game's language. Without the
key, only the icons. AES is in Python, as the AppImage has no crypto package.

When the game unlocks a trophy, the runtime records it in earned.tsv, the first time only, and
the overlay shows a card in the top-left corner in the style of the PS4's: the trophy's icon,
"You have earned a trophy." and its name. The platinum follows once every other trophy is
earned. The launcher's Trophies page lists them all, earned first, with the date; hidden ones
stay hidden until earned. BB_TROPHY_PREVIEW=<id> shows a trophy's notice at the game's first
trophy call, to see how it looks.
2026-10-11 15:35:42 +01:00
ayozetr e3c51c48aa Discord Rich Presence: the area and the co-op partners
Discord shows the area the player is in, and under it "Co-op with <partners>", "Online" or
"Offline". The game writes both into BB_PRESENCE_DIR, which run.sh makes new for each start:

- The runtime takes the area from the MapStudio files the game opens, one per map it loads.
  The player's own map comes first, then the maps around it (Central Yharnam brings the
  Forbidden Woods), so a map opened 5 s or more after the previous one starts a new load and
  names the area. It changes at loading screens only.
- The online module writes the online IDs of the players it has an established connection
  with, when a connection is established or removed.

The area names are those of Smithbox's Bloodborne map aliases (MIT); the Chalice Dungeons are
one name.
2026-10-11 15:11:09 +01:00
ayozetr 33393185b9 Support on Ko-fi: the About page, the README and GitHub's Sponsor button
A "Support this project" row in the launcher's About page opens ko-fi.com/ayozetr, the
README has a Support section, and .github/FUNDING.yml shows the link in GitHub's Sponsor
button.
2026-10-11 14:51:58 +01:00
ayozetr c294942db4 Launcher: the game folder names the release that is in it
The row said "Game folder (CUSA03173)" and the check's tooltip "Bloodborne CUSA03173,
version 1.09" whatever the folder held, from when only the European Game of the Year edition
ran. Both now name the title ID in the folder's param.sfo (CUSA00900, ...), or none without a
game.
2026-10-11 14:33:28 +01:00
obrikash 6c6974a742 Fix Vulkan startup when optional DLSS device extensions fail 2026-10-11 13:34:39 +01:00
ayozetr 83246710d7 0.51-linux.1, and what each tool is for
The launcher's version for the About page's updates. tools/README.md tells the tools the port,
the launcher and the AppImage use from bbport's development tools, kept as they are so that
its updates merge cleanly.
2026-10-11 02:53:20 +01:00
ayozetr b71eb7cfee Online: your own co-op server
shadPS4's public server never answers on its UDP port, so summons through it cannot connect
(deadinside28/bloodborne_pc#121). The Online page now chooses between that server and your
own, a shadnet-p2p server for a group of friends: its address and WebAPI, and seamless co-op,
which the server has to have on as well. The co-op port is a setting of its own instead of
SHADPS4_P2P_PORT in the extra variables, and the firewall warning uses it and says that UPnP
opens the port on the router only.

Test the connection pings the shadNet server's UDP port the way the game does and shows the
public address the server sees, or that it does not answer. It uses a made-up online ID: the
server keeps one address per account, and the player's own would replace the address of a
game running at the same time.

tools/shadnet-server builds shadnet-p2p in Docker on Debian 13, for the Qt 6.8 it needs
(Ubuntu 24.04 has 6.4), and keeps the configuration and accounts in a volume. With the
server's Tailscale address, it waits for the address before starting: shadnet-p2p keeps
running without its listeners when the address is missing.

Checked against shadnet-p2p 5a849a1: login, the UDP ping, matchmaking and a summon sign
reaching its summon service. docs/SERVER.md is the guide; ONLINE.md and TROUBLESHOOTING.md
cover online play and the common problems.
2026-10-11 01:41:53 +01:00
ayozetr b6c25e1fb4 Merge upstream 0.51 (master)
The online module from upstream replaces this fork's copy: the same shadp2p libraries, now
in gpu/bbnet as libbbnet.so, loaded only for online play, and UPnP opens the co-op port on
the router. Kept from this fork: game region, firewall warning, About page and updates,
cheats, Discord, title-ID mod folders and the sharun AppImage, which now bundles
libbbnet.so (BB_NET_MODULE). DLSS: the launcher's choice of NVIDIA's library looks in
BB_DLSS_DIR, the AppImage's copy. The launcher's new texts are translated.
2026-10-11 00:28:06 +01:00
ayozetr 0e2b626358 Mods and patches: every retail release, and a guide in English
A mod wrapped in a title ID folder was found only as CUSA03173/dvdroot_ps4; one made with the
US release (CUSA00900/dvdroot_ps4) was not. Any title ID folder is accepted now. Third-party
patch files listed six of the nine retail releases: a file for the Japanese release or the US
and Japanese The Old Hunters editions was skipped. The list is now the game check's.

docs/MODS.en.md is the guide to mods and third-party patches in English, based on bbport's
docs/MODS.md and checked against the code; the README and docs/README.md link to it, and the
Russian original moves to the development notes.
2026-10-10 22:33:31 +01:00
ayozetr b060b9fa2b Document which game releases run, and which have been checked
docs/COMPATIBILITY.md lists the nine retail releases the launcher accepts, with their title and
content IDs (from deadinside28/bloodborne_pc#117), and which ones have had their 1.09
executable checked: the US release (CUSA00900) and the European Game of the Year edition
(CUSA03173). The others are accepted on the expectation that they share that executable, and
the image hash still decides. It also lists what was tested with this fork. The README's note
on game files links to it.

game_check.py said the Asian The Old Hunters Edition (CUSA03023) shares the executable; nobody
has checked it, so it now says what is known.

docs/README.md is an index of docs/: what is for players, what is this fork's, and bbport's
development notes by topic, which stay as they are so that upstream updates merge cleanly.
2026-10-10 22:26:45 +01:00
ayozetr 18a7dd7e39 AppImage: only what the port needs, and DLSS's files intact
quick-sharun's OpenGL and Vulkan deployment copies every libEGL*/libGLX* library and every ICD
manifest of the build host. Built on a PC with NVIDIA's driver, the AppImage carried that
driver's user-space libraries (one exact version, 615.71.09 in 0.4-linux.1) and manifests,
which load next to the player's own driver and cannot match another kernel module. They are
now removed before the AppImage is made; NVIDIA's driver always comes from the host, where
launcher/bbport_vulkan.py points Vulkan.

quick-sharun also strips and patches every library in the AppDir, DLSS's among them. NVIDIA's
DLSS library is signed: changed, NGX refused it (0xbad00012) and DLSS was unavailable in the
AppImage on an RTX 3060 that runs it from the repository. The DLSS files are now copied after
quick-sharun, as built.

DEPLOY_PYTHON copied the build host's whole site-packages (scipy, Qt bindings, matplotlib, ...:
431 MB). Only PyGObject, pycairo and Pillow stay, which is all the launcher imports. The
AppImage goes from 342 MB to 225 MB.
2026-10-10 21:38:02 +01:00
ayozetr 28a9233034 Launcher: About page, and updates from this fork's releases
A new About page shows the version and links this project, the original bbport by
deadinside28 and Supermedo's Windows port, and names the license (GNU GPL v2 or later, no
game files included).

It also checks GitHub for the latest release: at start, unless turned off, and with a Check
now button; a newer release at start is announced with a toast. From the AppImage, Update
downloads the release's AppImage next to the running one and renames it over it (a failed
download leaves the old one as it was); the running copy keeps its mount, and Restart starts
the new one once this launcher has exited (with the same application ID still held, the new
one would only raise the old window). From a git checkout, or an AppImage in a folder that is
not writable, the button opens the release page instead.

The version lives in launcher/bbport_update.py (0.5-linux.1); tags follow
<upstream version>-linux.<n>, and a release carries the AppImage as an asset.
2026-10-10 21:38:02 +01:00
ayozetr 9c056c437f Launcher: warn when a firewall blocks co-op
Co-op peers connect straight to each other over UDP, and UPnP's discovery replies come from the
router on random source ports. With ufw enabled or firewalld running, both are dropped, and
nothing in the launcher said why co-op could not connect.

The Online page now names the firewall and gives the commands: pin the P2P port with
SHADPS4_P2P_PORT=3658, open it, and with ufw let the router's answers in (the default gateway
read from /proc/net/route). Both are detected without root.
2026-10-10 21:38:02 +01:00
ayozetr 2683273a17 Launcher: game region
Bloodborne picks a language's regional text from USER_DEFINED_PARAM_1 in param.sfo: 13 Europe
(Spanish from Spain), 2 the Americas (Latin American Spanish). A US dump played in Spanish showed
the Latin American text, and the only way to change it was editing the dump's param.sfo.

The launcher's Game region (As in the game dump, Europe, the Americas) sets BB_REGION, and
content_profile.py writes the matching parameter for the launch; the dump stays as it is.
2026-10-10 21:38:02 +01:00
ayozetr 4d297b96bb Translate 0.5's new texts, and the game's System menu pages
The launcher's new texts (memory model, log export, monitors, the offline patch, damaged game
files) in Spanish, Italian, French, German and Brazilian Portuguese; Portuguese also gets the
52 texts it never had (controls, game check, frame limit, memory options, diagnostics).

The Display and Effects pages in the game's System menu were English or Russian only. They now
follow the game's language (BB_LANGUAGE, else the desktop's locale, as the runtime tells the
game), with the texts in bbport_menu_i18n.h. The README lists 0.5's new features.

tests/test_i18n.py now checks that every text the launcher translates exists in English,
Spanish, Italian, French, German and Portuguese, and that translations keep the placeholders:
the 18 texts 0.5 added had slipped through unnoticed.
2026-10-10 21:38:02 +01:00
ayozetr 917c4bdd20 Record large copy lists in slices
Loading an area on NVIDIA with 0.5, 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 assertion stopped the game. Buffer copies, guest chunk copies and
buffer-to-image uploads are now recorded as one command per 64 KiB of regions.
2026-10-10 21:38:02 +01:00
ayozetr 8e24cfdcf5 Merge main: the region fix and every retail release
main corrected the regions (CUSA00900 is the US release, CUSA03173 the EU GOTY) and accepts
every retail release; the 0.5 merge still had upstream's older texts. Conflicts take main's
side; the Chinese launcher text of the old message is replaced by the new one.
2026-10-10 21:38:02 +01:00
ayozetr cfede387ed Merge upstream 0.5 (master)
0.5 on top of 0.5-pre4: the memory model chosen by GPU (the new one on AMD, the 0.3 one
elsewhere, or either by hand in the launcher), the command decoder, game profiles, GPU
timestamps, less VRAM churn, the finished Display and Effects pages of the game's System menu,
NVIDIA EGL discovery and RADV nohiz for older AMD GPUs.

Conflicts: README.md keeps this fork's own; tools/gpu_capabilities.c's header now lists both
the gamepad type (button prompts) and upstream's new --device.
2026-10-10 17:37:45 +01:00
ayozetr f44390da19 Game check: every retail release of Bloodborne
The list of content IDs posted by h0kd33 in deadinside28/bloodborne_pc
PR #117 covers every Bloodborne release: the US, EU, UK, Japanese and
Asian ones and the four editions with The Old Hunters. A base game of
any of them without the update is now asked for 1.09 instead of being
called another edition; whether the executable runs is still decided by
its image hash. The message is reworded in every launcher language.
2026-10-10 16:02:32 +01:00
ayozetr c89f4d432c Fix the regions: CUSA00900 is the US release, CUSA03173 the EU GOTY
CUSA00900 was called the EU release and CUSA03173 the US one, the other
way round: their content IDs are UP9000-CUSA00900 (America) and
EP9000-CUSA03173_00-BLOODBORNE0000EU (Europe, Game of the Year edition).
Only names change: the README, BBPORT.md, the release template, the
game check's texts and the launcher's message in every language.
2026-10-10 15:21:24 +01:00
ayozetr 4fd1cd6ee6 Restore what the 0.5 merge dropped: menu languages, cheats, co-op setup
Resolving the merge, three files of the in-game menu were taken whole
from upstream to settle conflicts that were only about DLSS, and
probe.c to drop the old add-on profile. That also dropped this fork's
own changes in them: the menu's Language selector and its Spanish,
Italian, French, German and Portuguese texts, the cheats and gameplay
tweaks, and the bbnet_configure call that gives the co-op code the
game's identity. With online play on, the game stopped at start
(ElfInfo GameSerial assertion). Now merged line by line, with upstream's
version only in the DLSS hunks.
2026-10-10 15:12:23 +01:00
ayozetr cfe7af67a5 Add to Steam: Flatpak Steam
Flathub's Steam keeps its data in ~/.var/app/com.valvesoftware.Steam
and starts non-Steam games inside its sandbox, which has neither the
game folder nor the host's drivers and cannot mount an AppImage. The
launcher now finds it, closes and starts it through flatpak run, and
writes a shortcut that runs the game on the host with
flatpak-spawn --host. That needs Steam to talk to org.freedesktop.Flatpak:
the launcher explains what the permission allows and grants it
(flatpak override --user) only when the player agrees.

A flatpak-spawn shortcut is only replaced when it is this port's (same
name, runs --play), so other launchers' games in Steam are left alone.
2026-10-10 14:10:32 +01:00
ayozetr 124384eac1 Merge upstream 0.5-pre4 (testing)
bbport 0.5-pre1 to pre4: the memory module (now on NVIDIA too), native
GPU copies, background pipelines, settings in the game's System menu,
DLSS, DLC folders and the damaged-extraction check.

Where both sides did the same thing, upstream's version is kept:
- DLSS: upstream's bridge and loader (libraries next to bb-probe, built
  by build.sh with DLSS_SDK_ROOT). BB_DLSS_DIR still overrides the
  folder, for the AppImage; tools/build_dlss.sh now installs into out/.
- DLC: upstream's <user>/addcont/<title>/<label> reporting. The
  launcher's DLC folder (BB_DLC_DIR) is linked there by run.sh, a real
  folder in its place left alone; our content.bin add-on list is gone.
- The memory-model switch is no longer AMD-only, as upstream has it.

Kept from this fork: launcher and in-game menu languages (Simplified
Chinese joins them), the game check for the EU and Asian releases and
the 60 fps-patched eboot (as sent upstream in PR #117), button prompts
and the controller type in bb-gpu-capabilities --gamepads, which also
lists displays now. Upstream's README changes went into BBPORT.md.
2026-10-10 14:07:39 +01:00
ayozetr 5227432b1b Git: keep game files and the mods folder out, quiet FSR-Vulkan
The mods folder of runs from source (mods/) holds the user's modded game
files and was not ignored; neither were dumps and their parts
(eboot.bin, param.sfo, *.pkg, *.dcx, *.7z, CUSA* folders). The repository
never takes any of them.

FSR-Vulkan always showed as modified because build.sh applies
gpu/patches/fsr-vulkan to its working tree: it is now ignore = dirty.
2026-10-10 03:49:42 +01:00
ayozetr 81db68f552 Docs: release notes template, credit for the keyboard prompts
docs/RELEASE_TEMPLATE.md fixes the format of every release: tag and
title, requirements, download with its SHA-256, New / Improved / Fixed,
Upgrading, Known issues, and links to the changes and credits.

The credits now name Abken's KBM Icons Generator (Nexus, Bloodborne mod
537), which showed which textures keyboard prompts need; bbport draws
its own and uses nothing of it.
2026-10-10 03:48:43 +01:00
ayozetr f723f6a972 Button prompts: Xbox, Switch and keyboard, and a cleaner gestures menu
Controls get a Button prompts row (automatic, PlayStation, Xbox,
Nintendo Switch, keyboard) and a switch for the gestures menu without
the motion-control icons. Automatic follows the controller in use: bb-
gpu-capabilities --gamepads now also prints SDL's controller type, and
with no controller the keyboard's prompts are used.

The Xbox and Switch prompts and the gestures menu are Dommo's (Nexus,
Bloodborne mod 30, whose permissions allow reuse with credit). Only what
the mods drew is in assets/menu_prompts: the 20 icons as their BC7
blocks, the strip of big prompts from the UI atlas, and the edits to
gesturetop.gfx; tools/extract_prompt_assets.py rebuilds them from the
mods' ZIPs. Keyboard prompts are drawn with Pillow for the keys bound in
bbport.ini.

At start, scripts/menu_prompts.py puts them into the player's own
menu/common.tpf.dcx (scripts/menu_textures.py: DCX/TPF, PS4 tiling, a
small BC7 encoder) without touching any other texture, caches the
result, and mods.py lays it under the user's mods (--ui-mods). The
AppImage and the Nix package now carry Pillow and DejaVu Sans Bold. The
README also says the in-game menu speaks Portuguese, since c428122.
2026-10-10 03:38:39 +01:00
ayozetr 82a96e0d25 Support the EU release (CUSA00900)
The EU 1.09 executable is the same image as the US one: with a clean
eboot.bin the EU dump passes the check and plays, gestures menu
included. The check now takes CUSA00900 as a supported title, so an EU
base game without the update is told to merge 1.09 instead of being
called another edition. README and BBPORT.md name both releases; the
latter also notes that run.sh follows the desktop's locale without
BB_LANGUAGE (left out of the previous commit).
2026-10-10 02:39:04 +01:00
ayozetr 71717863d5 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 same way the launcher does: Japanese,
English, French, Spanish, German, Italian or Russian, English otherwise.
2026-10-10 02:37:02 +01:00
ayozetr c4281227da In-game menu: Brazilian Portuguese
The launcher already speaks pt_BR, but the overlay fell back to English
for it. Adds Português (Brasil) to the menu's Language selector and a
pt_BR column to the overlay translations; the launcher's pt_BR (and any
pt_* locale) now reaches the menu through BB_MENU_LANGUAGE.
2026-10-10 02:35:32 +01:00
ayozetr ee20c8b147 Game check: recognise the eboot with the 60 fps patch baked in
Some dumps (seen on EU CUSA00900) ship the 1.09 eboot.bin with Lance
McDonald's 60 fps patch already applied. Apart from that patch it is the
supported executable, but one of its edits inverts a branch in the menu
code (+0xf6d90d) and the game crashes as soon as the gestures menu opens.

The check now names this image and asks for a clean 1.09 eboot.bin,
instead of the misleading "other edition" message; bbport has its own
60 fps. Launcher message in en/es/it/fr/de.
2026-10-10 02:27:24 +01:00
ayozetr a39a0073de Launcher: game language follows the desktop by default
The game takes its text and voices from the PS4 system language, which
the launcher set to English unless changed. System language now starts
on "System": the desktop's locale picks Japanese, English, French,
Spanish, German, Italian or Russian, and English for anything else.
A language chosen in the launcher still wins.
2026-10-09 23:38:24 +01:00
ayozetr 09a1e4981a DLC folder: report dumped add-ons to the game
The base game (not the GOTY edition) learns which add-ons were bought
from sceAppContentGetAddcontInfoList, which always answered none. Their
data ships with the 1.09 update; a dumped add-on is just its sce_sys.

- content_profile.py --dlc DIR lists the add-on folders of this title
  (CATEGORY ac, CONTENT_ID of the same title id) into content.bin, now
  BBCONT02 with the entitlement labels when there are any.
- The runtime returns them as SceAppContentAddcontInfo entries (status
  "no extra data" for key-only folders, "installed" otherwise).
- run.sh passes BB_DLC_DIR; the launcher has a DLC folder row showing
  how many add-ons it found, translated in every launcher language.
2026-10-09 23:19:51 +01:00
ayozetr 992fc95ed1 New README for the fork
What the fork adds and how to play, build, play online and enable DLSS, with
credits to bbport (deadinside28) and the Windows port (Supermedo). The original
README moves to BBPORT.md (and its Russian version to BBPORT.ru.md) for its
technical details.
2026-10-09 21:06:24 +01:00
ayozetr 851712537b Discord Rich Presence
"Playing Bloodborne" on the player's Discord profile while the game runs, online
or offline, with the time played and the icon. scripts/discord_presence.py talks
to Discord's local IPC socket (also the Flatpak, Snap and Vesktop ones), keeps
looking for Discord when it is not running and ends with the game; run.sh starts
it unless BB_DISCORD=0, and the launcher has a switch for it.
launcher/bloodborne.svg: the icon redrawn as vector art, for the 1024 px assets
Discord needs.
2026-10-09 18:58:48 +01:00
ayozetr e289b722b5 DLSS Super Resolution for NVIDIA RTX on Linux
From the Windows port of Supermedo/bloodborne_pc (commit 9514b19; the bridge is
adapted from IFreemz/shadPS4-Bloodborne-DLSS-FSR): a new upscaler "dlss" that
gets the inputs FSR 3 gets, falls back to FSR 3.1 when DLSS fails, and changes
nothing without the DLSS files, on other GPUs or with BB_DLSS=0.

Linux: the bridge builds as libbbport_dlss.so against the SDK's libnvsdk_ngx.a
(only BbDlssGetApi exported) and is loaded with dlopen from BB_DLSS_DIR or the GPU
library's directory, next to NVIDIA's libnvidia-ngx-dlss.so.<version>.
tools/build_dlss.sh fetches the SDK files (v310.9.1, cached) and puts the bridge,
the library and its license into out/gpu; the AppImage ships them when present.
The launcher lists DLSS with a note on its files and the RTX requirement. Merged
with 0.4: FSR 4 keeps its reactive blend, Scheduler::WaitSubmittedWork.
2026-10-09 18:42:42 +01:00
ayozetr c7cae2a585 Launcher: online account checks before playing
As in the Windows launcher: with an account filled in and Play online off, ask
whether to play online or offline; an email address as the Online ID is refused
with a note (the NPID is the account name) and the Online page is shown.
2026-10-09 18:35:30 +01:00
ayozetr 7d53c5e9d6 AppImage: bundle Python's ssl and hashlib modules
quick-sharun leaves them out by default; the launcher's connection test and
scripts/online.py talk HTTPS to the co-op servers.
2026-10-09 18:32:08 +01:00
ayozetr 7dfbf49367 Online co-op on Linux
Online play through shadNet and The Hunter's Dream, from the Windows port of
Supermedo/bloodborne_pc (commit e076119), which built it from the network and PSN
libraries of shadp2p (github.com/Wozzardman/shadp2p): sign-in, matchmaking,
signaling, WebAPI and HTTPS. With BB_ONLINE=1 the game's network and PSN imports
go to these libraries instead of the offline stubs; trophies, voice and the store
dialogs stay in the runtime. It uses the same servers as the Windows version, so
players of both play together.

Linux changes: UPnP through the system's miniupnpc (the Windows port linked it
there only), no wepoll or Windows libraries; scripts/online.py replaces run.py's
host overrides (game server -> community server, summon signs -> the shadNet
WebAPI when it has the summon service) and is called by run.sh. Launcher: an
Online page (play online, game server, co-op server and WebAPI, shadNet account,
UPnP, connection test), translated; settings.json is now private (0600) since it
holds the shadNet password. tests/test_online.c checks the import routing.
2026-10-09 18:26:14 +01:00
ayozetr 52a6daf288 Add to Steam: replace this port's shortcut from another place
A shortcut named like ours with the same launch options (--play) is updated
rather than duplicated, so moving from a checkout to the AppImage keeps one
entry; other shortcuts with the same name (shadPS4) stay as they are.
2026-10-09 18:10:45 +01:00
ayozetr 912641c298 AppImage without Nix (packaging/appimage-sharun.sh)
Collects the build, the scripts, the GTK4 launcher and every library they load
(glibc, Python with PyGObject, GTK 4, libadwaita, SDL3, FFmpeg, Mesa's Vulkan
drivers) with quick-sharun from pkgforge-dev/Anylinux-AppImages, pinned and
checksummed. Downloads the FSR 4 assets when they are missing. Runs the launcher,
or the game alone with --play, from ~/.local/share/bbport.
2026-10-09 18:10:20 +01:00
ayozetr dca76e386f Add to Steam from the launcher
A non-Steam shortcut in every Steam account's shortcuts.vdf that runs the launcher
(or the AppImage) with --play. Steam is closed first when it runs and started again
afterwards. Under Steam, --play restores the system's LD_LIBRARY_PATH.
shortcuts.vdf handling and the icon come from the Windows launcher of
Supermedo/bloodborne_pc. bb-launcher.sh now passes its arguments through nix-shell,
and the desktop entry uses the icon.
2026-10-09 17:49:35 +01:00
ayozetr 0b465f8ee1 In-game menu in Spanish, Italian, French and German
The Language selector lists six languages; menu_language in bbport.ini and
BB_MENU_LANGUAGE take their codes (en, ru, es, it, fr, de), and the launcher's
language or the system locale picks one until the menu sets it. The translations
of the English texts are in gpu/shim/bbport_menu_i18n.h; missing ones stay English.
2026-10-09 17:45:56 +01:00
ayozetr 82782d8155 Launcher in Spanish, Italian, French and German
Every launcher string translated; the system locale picks them, and so does the
launcher language selector. Strings missing in a translation fall back to English.
2026-10-09 17:45:56 +01:00
ayozetr 89ea3deb70 Cheats and gameplay tweaks page in the launcher and in-game menu
Community patches for 1.09 already in patches/Bloodborne.xml (never die, stealth,
silent, Rally that never fades, enemy control, no Rally, camera distance, no camera
auto-rotation, easier running, ragdoll physics), switched from bbport.ini.
Ported from the Windows fork (Supermedo/bloodborne_pc).
2026-10-09 17:45:56 +01:00
149 changed files with 12327 additions and 2053 deletions

No files matched your search

+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.
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).
+9
View File
@@ -23,6 +23,8 @@ 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
@@ -30,3 +32,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
# Game files and dumps: never in the repository
eboot.bin
param.sfo
*.pkg
*.dcx
*.7z
/CUSA*/
+2
View File
@@ -1,6 +1,8 @@
[submodule "gpu/third_party/fsr-vulkan"]
path = gpu/third_party/fsr-vulkan
url = https://github.com/FireBurn/FSR-Vulkan.git
# build.sh applies gpu/patches/fsr-vulkan to its working tree
ignore = dirty
[submodule "gpu/third_party/imgui"]
path = gpu/third_party/imgui
url = https://github.com/ocornut/imgui.git
+436
View File
@@ -0,0 +1,436 @@
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
> 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)
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:
- **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; without it the game runs on the old
memory model, as in 0.3.
One step remains to the full Wine + DXVK model: make the new mode the default and remove the old
memory model (see below).
> **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.
## 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.** 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). 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 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. **It may crash**; hardware testing has been performed on an
RX 7800 XT, and performance depends on the scene and settings. On AMD the game's memory is kept
in a sparse buffer (the arena). **On NVIDIA, Intel and others** it goes through the layer's
memory module: the game's memory is bound in place and VRAM copies are separate buffers, with
no sparse rebinding. This avoids the sparse-binding path associated with reported freezes
on a GTX 1660 Ti. On NVIDIA the module runs on a tester's GTX 1660 Ti, its speed is still
being checked; not tested on Intel. Without the launcher: `BB_PC_MODEL=1`; to choose the memory
path: `BB_GUEST_MEMORY=host|dmabuf`; the memory module: `BB_LAYER_MEMORY=1/0`.
[0.5-pre4 changes](docs/CHANGES_0.5-pre4.md): no stutter of the memory module (NVIDIA) while areas
stream in, shader compilation in the background, VRAM on 4–6 GB cards, damaged game extractions
reported, the settings menu as in 0.3. [0.5-pre3](docs/CHANGES_0.5-pre3.md): the memory module on
NVIDIA — VRAM copies, one VRAM budget with the texture cache, watched blocks. [0.5-pre2](docs/CHANGES_0.5-pre2.md): the memory
module, native GPU copy shader, portable GPU completion labels, and shader-cache migration.
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.
- **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
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
- **FSR 3.1** (FireBurn/FSR-Vulkan).
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
RDNA2/3 included (see Requirements).
- **FSR 4.1.1**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes are
replayed natively on Vulkan; the output is **bit-exact** with the DLL. Two variants, as in
the DLL: INT8 on any GPU with the required shader features, and FP8 matrices on RDNA4 (RX
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`.
- 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.
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
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.
- **Settings inside the game's own menu:** *System → Display / Game effects / Game patches*
(after *Screen/Sound*), built and drawn by the game itself — output resolution, upscaler,
preset, sharpness, FPS counter, model detail and the effect patches, in the game's language.
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
advanced settings, with gamepad, keyboard and mouse.
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
## How it differs from shadPS4
| | shadPS4 | bbport |
|---|---|---|
| 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: 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 |
| 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 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 |
| 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.
### Wine + DXVK for one game
- **CPU.** The PS4 CPU is x86-64, so the game's code runs directly on the PC's CPU, with no
emulation and no instruction translation.
- **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.
- **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; 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](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 is not tested on NVIDIA yet.
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 US release (`CUSA00900`) runs too: its 1.09 executable is the same as this European
Game of the Year edition's.
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). 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.
## Build and run
```bash
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
```
or the launcher (pick the game folder, settings, *Start*):
```bash
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
```
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
shader cache go to `user/` (the launcher lets you choose another folder); settings to
`bbport.ini`. A gamepad is used through SDL3 (the launcher's *Controls → Controller* picks one
when several are connected; `BB_GAMEPAD=<GUID or part of the name>`). The keyboard works too,
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. 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 named by its entitlement label (`SPEXPANSIONDLC03/sce_sys/...`).
Their data ships with the 1.09 update, so a folder with only `sce_sys` is enough. `run.sh` links
it as `user/addcont/<title id>`, where the runtime finds add-ons (see **DLC** below); a real
folder already there is kept. 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.
**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
to FSR. FSR Native AA adds reconstruction on top of full-resolution rendering and can
be slower than disabling AA. TAA also adds work compared with no temporal AA.
The RCAS switch and the 0–2 sharpness control also work with TAA. Sharpening runs after
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.
**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).
**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, Brazilian Portuguese, Spanish, Italian, French, German or Simplified Chinese (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).
**Game region:** *Game region* in the launcher, `BB_REGION=eu|na` for `run.sh`: the regional text
of a language, such as Spanish from Spain (`eu`) or Latin American Spanish (`na`). It sets the
game's `USER_DEFINED_PARAM_1` (13 Europe, 2 the Americas) for the launch; by default the dump's
`param.sfo` decides.
**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
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
`dvdroot_ps4/font/` first. Startup rejects missing or empty font files instead of launching
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.
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.
**Upscaler assets** (not included; FSR 3.1 needs none):
```bash
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source by Q2RTX)
# FSR 4.1.1, from your own AMD DLLs (e.g. OptiScaler's FSR4_LATEST), needs GE-Proton 10 or newer:
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
```
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Upscaler → FSR 4.1.1 from your
own AMD DLL → Choose DLL…*, then pick `amd_fidelityfx_upscaler_dx12.dll` version 4.1.x: from
OptiScaler's `FSR4_LATEST` folder or from a game with FSR 4.1. That one file is enough: AMD's DLL
exports the FidelityFX API itself, so no loader is needed (only a DLL without these exports would
be recorded through `amd_fidelityfx_loader_dx12.dll`, and the launcher would ask for it). The DLL is checked at once,
without a long capture; these do not fit: AMD's official FSR 4.0.x (e.g. Pragmata's 4.0.3 —
AMD enables it on RDNA4 only, and under Proton it does not start on other GPUs) and community
builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pass count). Then
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,
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
umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downloaded the first
time, ~1.7 GB); the AppImage runs the recording on the system itself, through the user's systemd
(`systemd-run --user`), since the system's `/nix` is out of its sight. Recording on the Steam Deck is not verified yet; if the DLL does not
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.
On Linux, DLSS also needs NVIDIA's CUDA/UVM runtime. If device creation fails with the DLSS
extensions, the renderer retries without them and uses FSR 3.1. If `/dev/nvidia-uvm` is missing,
load the installed driver module with `sudo modprobe nvidia_uvm` and restart the game. Installing
the distribution's `nvidia-modprobe` package lets NVIDIA's libraries load the module when needed.
If `modprobe` reports `Key was rejected by service` with Secure Boot enabled, enroll the
module's signing key using the distribution's [MOK procedure](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
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.
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.
**FP8 on RDNA4.** When vkd3d-proton offers FP8 cooperative matrices (RDNA4), the DLL runs
another variant of its passes: other shaders and weights, other dispatch sizes. The build records
both, INT8 into `fsr4_411/` and FP8 into `fsr4_411/fp8/`, and the game picks FP8 itself when the
GPU has FP8 matrices (`VK_EXT_shader_float8`); the log says `Upscaler: FSR 4.1.1 replay, FP8 …`.
`BB_FSR411_VARIANT=int8` forces INT8. Checked on an RX 7800 XT through vkd3d-proton's FP16
emulation of FP8 (`BB_FSR4CAP_FP8=1` records it there too, `fp8emu/`): bit-exact with the DLL;
**not yet run on RDNA4 hardware.**
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
`dist/Bloodborne-bbport-x86_64.AppImage`; data in `~/.local/share/bbport`, `--play` starts the
game without the launcher window (Game Mode). FSR 4.1.1 models are not packaged: build them with
the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
(`BB_PACKAGE_FSR411=1` bundles a local `fsr4_411` into an AppImage for your own devices). On the Steam Deck pick the 1280×720 output (the
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
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
AppImage*), set the launch options to
```
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
```
The AppImage then unpacks itself (~2 GB, `~/.cache/appimage_extracted_*`) on the first start
(~10 s) and reuses that copy afterwards; a new AppImage version gets a new copy, the old one can
be deleted. Without `TMPDIR` it would unpack into Steam's `/tmp`, which is in RAM there. Add
` --play` after `%command%` to skip the launcher.
**NVIDIA in the AppImage:** startup discovers the host's installed 64-bit NVIDIA Vulkan ICD
and exposes its vendor libraries alongside the bundled AMD/Intel drivers. This keeps the
NVIDIA userspace driver matched to the host kernel module. Standard Linux distributions keep
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
the package with:
```bash
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
```
A user reported successful startup with FSR 3 on a GTX 1060 6GB (Fedora 44, NVIDIA
580.178.04); selecting FSR 4 caused a black window. Use FSR 3 on this configuration.
MangoHud is bundled in the AppImage; enable its checkbox in the launcher. If MangoHud is also
installed system-wide, or Steam's performance overlay is on (Steam Deck game mode), only one
overlay is drawn (two drew doubled, offset text).
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,
0, the old model as in 0.3, is the default), `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
it is stuck in), `BB_GPU_PROFILE=1` (GPU time per
pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
`BB_FRAMES_AHEAD=N` (how many frames the GPU command thread may run ahead of the GPU; 1 by default,
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_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).
**Online play (shadNet).** A separate module, `out/gpu/libbbnet.so` (`gpu/bbnet`: shadPS4's network
and PSN libraries from shadp2p, a shadPS4 fork, with the shadNet client), 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`): messages, bloodstains and ghosts come
from The Hunter's Dream, bells and summons go through a shadNet server (`srv.shadps4.net:31313` by
default) with a shadNet account (Online ID and password). Offline the module is never loaded; a
build without it plays offline only.
## Repository layout
| Path | Contents |
|---|---|
| `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 |
| `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)) |
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`.
## Roadmap
- The new memory and translation model on by default once it is stable and tested on NVIDIA;
the old model is removed after that.
- Textures as objects created at load time (less guessing by address, uploads outside the
frame).
- Shaders translated ahead of time, at install, not during play.
- 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`); inputs exposed so that OptiScaler-style mapping works.
- Frame generation (FSR 3.1 FG first), 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.
## 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+),
[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,
[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`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
loader come from [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc), 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.
+12 -1
View File
@@ -2,7 +2,10 @@
# bbport — нативный порт Bloodborne для Linux
[English](README.md) · **Русский**
> README оригинального проекта (deadinside28/bloodborne_pc), сохранён ради технических
> подробностей. README этого форка — [README.md](README.md) (на английском).
[English](BBPORT.md) · **Русский**
bbport — это аналог Wine + DXVK, только для одной игры: *Bloodborne* для PlayStation 4
(CUSA03173, версия игры 1.09) на ПК с Linux x86-64. Оригинальный исполняемый файл игры
@@ -256,6 +259,14 @@ Experimental от октября 2026; Proton 11.0 из Steam и GE-Proton 9 —
векторы движения, jitter). Без библиотек, на других видеокартах или с `BB_DLSS=0` он недоступен,
а выбранный DLSS заменяется на FSR 3.1.
На Linux DLSS также нужен CUDA/UVM из драйвера NVIDIA. Если устройство не создаётся с
расширениями DLSS, рендерер повторяет попытку без них и использует FSR 3.1. Если отсутствует
`/dev/nvidia-uvm`, загрузи установленный модуль командой `sudo modprobe nvidia_uvm` и перезапусти
игру. Пакет `nvidia-modprobe` из репозитория дистрибутива позволяет библиотекам NVIDIA загружать
модуль по мере необходимости.
Если при включённом Secure Boot команда `modprobe` сообщает `Key was rejected by service`,
зарегистрируй ключ подписи модуля по [процедуре MOK своего дистрибутива](https://documentation.ubuntu.com/security/security-features/platform-protections/secure-boot/#mok-generation-and-signing-process).
AppImage, собранный с `DLSS_SDK_ROOT`, содержит мост и библиотеку NVIDIA (лицензия RTX SDK от NVIDIA
это разрешает в составе приложения; её текст — `NVIDIA-DLSS-LICENSE.txt` в пакете). Более новая
`libnvidia-ngx-dlss.so.<версия>` (из `lib/Linux_x86_64/rel` в SDK) выбирается в лаунчере:
+134 -375
View File
@@ -1,408 +1,167 @@
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.
# 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.
# bbport — a native Linux port of Bloodborne
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.
**English** · [Русский](README.ru.md)
> **No game files are included.** You need your own decrypted dump of Bloodborne with update
> 1.09, from a retail release: [which ones, and which have been checked](docs/COMPATIBILITY.md).
> This project is not affiliated with Sony Interactive Entertainment or FromSoftware.
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:
## Features
- **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 follow the PC model on AMD GPUs by default (the new memory and
translation model); NVIDIA and other GPUs keep the old memory model of 0.3 for now. The
launcher's *Memory model* switches either way.
One step remains to the full Wine + DXVK model: the new model on NVIDIA as well, then removing the
old memory model (see below).
> **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.
## 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 memory models** (launcher → *Mode → Memory and translation model*; *Auto*, the default,
picks by GPU):
- **The old memory model, as in 0.3 — the default on NVIDIA, Intel and others.** 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).
- **The new memory and translation model — the default on AMD.** 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). 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 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 AMD the game's memory is kept in a sparse buffer (the arena); on
NVIDIA, Intel and others it goes through the layer's memory module: bound in place, VRAM
copies as separate buffers, no sparse rebinding (NVIDIA's sparse binding froze the GPU for
seconds); `BB_LAYER_MEMORY=1/0` chooses by hand. Tested on an RX 7800 XT. On NVIDIA it can be chosen by hand (*New*); testers' results differ by scene,
some scenes are slower than with the old model, so it is not the default there yet. Not
tested on Intel. Without the launcher: `BB_PC_MODEL=1/0` (unset: by GPU, as *Auto*).
[Changes in 0.5](docs/CHANGES_0.5.md); the prereleases: [pre4](docs/CHANGES_0.5-pre4.md),
[pre3](docs/CHANGES_0.5-pre3.md), [pre2](docs/CHANGES_0.5-pre2.md).
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.
- **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
scene is jittered sub-pixel (Halton) and rendered at a reduced resolution; the upscaler fills
the output (720p for the Steam Deck, 1080p, 1440p or 2160p) and the UI is drawn natively at the output resolution.
- **FSR 3.1** (FireBurn/FSR-Vulkan).
- **FSR 4 (INT8, model v07)** on GPUs exposing the required Vulkan shader features —
RDNA2/3 included (see Requirements).
- **FSR 4.1.1**: AMD's 4.1.1 DLL is recorded once under vkd3d-proton and its passes are
replayed natively on Vulkan; the output is **bit-exact** with the DLL. Two variants, as in
the DLL: INT8 on any GPU with the required shader features, and FP8 matrices on RDNA4 (RX
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`.
- 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.
- **Multi-threaded GPU command processing.** The PS4 command stream is decoded on one thread
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.
- **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.
- **Overlay menu** (Insert or L3+R3), styled like the game's dialogs: everything above plus the
advanced settings, with gamepad, keyboard and mouse.
- **GTK4 launcher** and an **AppImage** for the Steam Deck.
## How it differs from shadPS4
| | shadPS4 | bbport |
|---|---|---|
| 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: 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 |
| 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 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 |
| 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.
### Wine + DXVK for one game
- **CPU.** The PS4 CPU is x86-64, so the game's code runs directly on the PC's CPU, with no
emulation and no instruction translation.
- **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.
- **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; 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](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.
- **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,
through shadPS4's public server or [one of your own](docs/SERVER.md).
- **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, Russian and Simplified Chinese, like the in-game menu (Insert or L3+R3).
- **Settings in the game's own System menu** as well (Display and Effects pages), in the game's
language.
- **Two memory models:** the new one, where the GPU works with the game's memory as in a PC game
(the default on AMD), and the one of 0.3 (the default elsewhere); the launcher switches them.
- **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.
- **Trophies:** a notice in the game, in the style of the PS4's, and a Trophies page in the
launcher with every trophy, from your own dump. Their names and descriptions need the PS4's
trophy key (shadPS4's TrophyKey), which you enter in the launcher; without it, only the icons.
- **Game effects** on and off, **mods** and community **patches**, loaded without changing the
game files ([how](docs/MODS.en.md)).
- **Add to Steam** in one click (native or Flatpak Steam), and **Discord Rich Presence** with the
area you are in and your co-op partners.
- **AppImage:** one file with everything inside, for any distribution and the Steam Deck. It
updates itself: the launcher's **About** page checks this repository's releases and installs
a new AppImage over the old one.
## 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 is not tested on NVIDIA yet.
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.
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). 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.
- 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.
## Build and run
## Getting started
### AppImage
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`.
### From source
On Arch Linux (or CachyOS, Manjaro…):
```bash
git clone --recursive https://github.com/deadinside28/bloodborne_pc.git bbport && cd bbport
bash build.sh # builds out/bb-probe and out/gpu/libbbgpu.so
BB_GAME_DIR=/path/to/CUSA03173 bash run.sh
sudo pacman -S --needed base-devel cmake ninja python python-gobject gtk4 libadwaita sdl3 \
vulkan-headers vulkan-icd-loader glslang spirv-headers spirv-cross ffmpeg boost fmt \
robin-map xxhash zydis miniz protobuf nlohmann-json openssl miniupnpc python-pillow ttf-dejavu
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
```
or the launcher (pick the game folder, settings, *Start*):
With [Nix](https://nixos.org), `bash build.sh` gets every dependency from `shell.nix` by itself.
```bash
bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to the app menu
```
### Online play
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
shader cache go to `user/` (the launcher lets you choose another folder); settings to
`bbport.ini`. A gamepad is used through SDL3 (the launcher's *Controls → Controller* picks one
when several are connected; `BB_GAMEPAD=<GUID or part of the name>`). The keyboard works too,
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. 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.
1. Register a shadNet account at [shadnet.shadps4.net](https://shadnet.shadps4.net), or get one
from the owner of your group's own server.
2. In the launcher, open **Online**, turn on **Play online**, choose the shadNet server and enter
your account name (not your email) and password. **Test the connection**: every line should
be green.
3. Start the game and choose online at the title screen.
**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.
Everyone in a session needs game version 1.09 and the same servers. Turn cheats off when you
play with others.
**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
to FSR. FSR Native AA adds reconstruction on top of full-resolution rendering and can
be slower than disabling AA. TAA also adds work compared with no temporal AA.
The RCAS switch and the 0–2 sharpness control also work with TAA. Sharpening runs after
temporal accumulation and leaves its history and HUD unchanged.
**Co-op needs your own server for now:** shadPS4's public server does not answer on its UDP port,
so summons do not connect through it (messages, bloodstains and ghosts do).
[docs/SERVER.md](docs/SERVER.md) sets one up with Docker and Tailscale; it has been tested with a
Linux and a Windows player.
**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.
**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).
**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.
Co-op connects the players directly over UDP. UPnP opens the port on the router by itself; with
ufw or firewalld on, the launcher's **Online** page says so and gives the commands: pin the
**Co-op port** (for example 3658), open it (`sudo ufw allow 3658/udp`), and for UPnP let the
router's answers in (`sudo ufw allow from <router> proto udp`). [More in docs/ONLINE.md](docs/ONLINE.md).
**Launcher language:** Russian, English, Brazilian Portuguese or Simplified Chinese (follows the system language by default).
### DLSS
**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
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
`dvdroot_ps4/font/` first. Startup rejects missing or empty font files instead of launching
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.
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.
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.
### DLC
**Upscaler assets** (not included; FSR 3.1 needs none):
The Game of the Year edition (`CUSA03173`) already includes The Old Hunters. With the 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 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. 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.
```bash
bash tools/fetch_fsr4_assets.sh # FSR 4 v07 (MIT, built from AMD's source by Q2RTX)
# FSR 4.1.1, from your own AMD DLLs (e.g. OptiScaler's FSR4_LATEST), needs GE-Proton 10 or newer:
bash tools/fsr4cap/build_assets.sh <amd_fidelityfx_upscaler_dx12.dll>
```
### Add to Steam
**FSR 4.1.1 from your own DLL.** The easiest way is the launcher: *Upscaler → FSR 4.1.1 from your
own AMD DLL → Choose DLL…*, then pick `amd_fidelityfx_upscaler_dx12.dll` version 4.1.x: from
OptiScaler's `FSR4_LATEST` folder or from a game with FSR 4.1. That one file is enough: AMD's DLL
exports the FidelityFX API itself, so no loader is needed (only a DLL without these exports would
be recorded through `amd_fidelityfx_loader_dx12.dll`, and the launcher would ask for it). The DLL is checked at once,
without a long capture; these do not fit: AMD's official FSR 4.0.x (e.g. Pragmata's 4.0.3 —
AMD enables it on RDNA4 only, and under Proton it does not start on other GPUs) and community
builds (4.0.2b, 4.1.1b and similar OptiScaler INT8 builds: another model and pass count). Then
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,
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
umu-launcher when installed. On NixOS umu-launcher comes from `nix-shell` (downloaded the first
time, ~1.7 GB); the AppImage runs the recording on the system itself, through the user's systemd
(`systemd-run --user`), since the system's `/nix` is out of its sight. Recording on the Steam Deck is not verified yet; if the DLL does not
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>`.
**Game → Add to Steam** puts Bloodborne in your Steam library; it starts with the launcher's
settings. Steam has to be closed while the game is added, and the launcher offers to do that.
Flatpak Steam runs games inside its sandbox, which has neither the game nor your drivers, so
the shortcut starts the game outside it with `flatpak-spawn --host`. That needs the
`org.freedesktop.Flatpak` permission for Steam, which the launcher asks for once: it lets Steam
start programs on your system.
**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.
### Button prompts
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.
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.
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.
## Support
**FP8 on RDNA4.** When vkd3d-proton offers FP8 cooperative matrices (RDNA4), the DLL runs
another variant of its passes: other shaders and weights, other dispatch sizes. The build records
both, INT8 into `fsr4_411/` and FP8 into `fsr4_411/fp8/`, and the game picks FP8 itself when the
GPU has FP8 matrices (`VK_EXT_shader_float8`); the log says `Upscaler: FSR 4.1.1 replay, FP8 …`.
`BB_FSR411_VARIANT=int8` forces INT8. Checked on an RX 7800 XT through vkd3d-proton's FP16
emulation of FP8 (`BB_FSR4CAP_FP8=1` records it there too, `fp8emu/`): bit-exact with the DLL;
**not yet run on RDNA4 hardware.**
If this fork is useful to you and you would like to support it, you can on
[Ko-fi](https://ko-fi.com/ayozetr). The port itself is deadinside28's work (see below).
**AppImage** (Steam Deck): `bash build.sh && bash packaging/appimage.sh` →
`dist/Bloodborne-bbport-x86_64.AppImage`; data in `~/.local/share/bbport`, `--play` starts the
game without the launcher window (Game Mode). FSR 4.1.1 models are not packaged: build them with
the launcher's button (see above); they go to `~/.local/share/bbport/fsr4_411`
(`BB_PACKAGE_FSR411=1` bundles a local `fsr4_411` into an AppImage for your own devices). On the Steam Deck pick the 1280×720 output (the
game is 16:9; on the 1280×800 screen it gets thin bars).
## Credits
**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
runs games without FUSE (NixOS: Steam's FHS sandbox; the AppImage then exits with *Cannot mount
AppImage*), set the launch options to
- [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.
```
TMPDIR=$HOME/.cache APPIMAGE_EXTRACT_AND_RUN=1 NO_CLEANUP=1 %command%
```
Full list of components and licenses: [BBPORT.md](BBPORT.md#credits-and-licenses).
The AppImage then unpacks itself (~2 GB, `~/.cache/appimage_extracted_*`) on the first start
(~10 s) and reuses that copy afterwards; a new AppImage version gets a new copy, the old one can
be deleted. Without `TMPDIR` it would unpack into Steam's `/tmp`, which is in RAM there. Add
` --play` after `%command%` to skip the launcher.
## License
**NVIDIA in the AppImage:** startup discovers the host's installed 64-bit NVIDIA Vulkan ICD
and exposes its vendor libraries alongside the bundled AMD/Intel drivers. This keeps the
NVIDIA userspace driver matched to the host kernel module. Standard Linux distributions keep
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).
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
./Bloodborne-bbport-x86_64.AppImage --vulkan-info 2>&1 | tee bbport-vulkan.log
```
A user reported successful startup with FSR 3 on a GTX 1060 6GB (Fedora 44, NVIDIA
580.178.04); selecting FSR 4 caused a black window. Use FSR 3 on this configuration.
MangoHud is bundled in the AppImage; enable its checkbox in the launcher. If MangoHud is also
installed system-wide, or Steam's performance overlay is on (Steam Deck game mode), only one
overlay is drawn (two drew doubled, offset text).
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,
0, the old model as in 0.3, is the default), `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
it is stuck in), `BB_GPU_PROFILE=1` (GPU time per
pass), `BB_FSR4_PROFILE=1` (GPU time per FSR 4 pass), `BB_UPSCALER=taa|fsr3|fsr4|fsr411|off|none`,
`BB_FRAMES_AHEAD=N` (how many frames the GPU command thread may run ahead of the GPU; 1 by default,
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_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).
**Online play (shadNet).** A separate module, `out/gpu/libbbnet.so` (`gpu/bbnet`: shadPS4's network
and PSN libraries from shadp2p, a shadPS4 fork, with the shadNet client), 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`): messages, bloodstains and ghosts come
from The Hunter's Dream, bells and summons go through a shadNet server (`srv.shadps4.net:31313` by
default) with a shadNet account (Online ID and password). Offline the module is never loaded; a
build without it plays offline only.
## Repository layout
| Path | Contents |
|---|---|
| `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 |
| `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)) |
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`.
## Roadmap
- The new memory and translation model on by default on NVIDIA too, once it is as fast there;
the old model is removed after that.
- Textures as objects created at load time (less guessing by address, uploads outside the
frame).
- Shaders translated ahead of time, at install, not during play.
- 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`); inputs exposed so that OptiScaler-style mapping works.
- Frame generation (FSR 3.1 FG first), 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.
## 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+),
[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,
[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`). The DLSS bridge (`gpu/dlss_bridge`, MIT) and
loader come from [Supermedo's Windows port](https://github.com/Supermedo/bloodborne_pc), 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.
GNU GPL v2 or later ([LICENSE](LICENSE)). Third-party components keep their own licenses. NVIDIA
and DLSS are trademarks of NVIDIA Corporation.
+16
View File
@@ -0,0 +1,16 @@
# Button prompts and gestures menu
By **Dommo**, from [Nexus Mods: Bloodborne, mod 30](https://www.nexusmods.com/bloodborne/mods/30)
(uploaded by goomab): *DS3 Style - Modern Xbox prompts*, *DS3 Style - Nintendo Switch prompts* and
*Stripped down gesture menu*. The mod page allows using its assets and uploading them elsewhere
with credit to the author, but not in anything sold.
Only what the mods drew is here, never the game's own files:
- `xbox/`, `switch/`: the 20 button icons (`KG_*.dds`, BC7 exactly as in the mods) and the strip
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.
+91
View File
@@ -0,0 +1,91 @@
{
"original_sha256": "5d7bae1451d97296e88a6add62b1fd9d1425901acc5c907e90f52354bc0b624e",
"result_sha256": "c77f7398c02613bd3d1a9613ce773fa8b6b74143053c6092b52a3c8d959dd868",
"edits": [
[
4,
1,
"2f"
],
[
2548,
1,
"18"
],
[
2556,
1,
"8e"
],
[
2563,
0,
"c9"
],
[
2564,
0,
"0034bca0480238"
],
[
3189,
1,
"92"
],
[
3229,
1,
"92"
],
[
3242,
5,
"207b2673a5e0"
],
[
3268,
1,
"98"
],
[
3281,
5,
"2077b30a7b80"
],
[
3519,
1,
"41"
],
[
3529,
1,
"33"
],
[
3539,
5,
"c900004000207d11920000"
],
[
5795,
1,
"ab"
],
[
5817,
1,
"1a"
],
[
5826,
5,
"c900004000207a015deba0"
],
[
6076,
12,
""
]
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+48
View File
@@ -0,0 +1,48 @@
# 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, dump the
add-ons too and choose their folder in the launcher (see the README's *DLC* section).
## 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.
- Both regional texts through *Game region*: Spanish from Spain (Europe) and Latin American
Spanish (the Americas).
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 @@
# Mods and third-party patches
Both are on the launcher's **Settings** page 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`, ...).
- One extra wrapper folder, as extracting an archive into a folder of its name leaves it:
`Mod name/Mod name v1.2/dvdroot_ps4/...` (readme files and pictures may sit next to it).
- 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`).
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 launcher's button prompts and gestures
menu work the same way, as files laid under the mods; they are settings of their own and stay
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.
- 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](MODS.md) (Russian), by deadinside28.
+77
View File
@@ -0,0 +1,77 @@
# Online play and co-op
Online play is a beta. Messages, bloodstains and ghosts come from the community game server,
[The Hunter's Dream](https://thehuntersdream.com). Bells and summons go through a shadNet server,
and then the players' games connect to each other directly. Linux and Windows players can play
together: everyone needs the game at version 1.09 and the same servers.
> **Co-op does not connect through shadPS4's public server.** It does not answer the UDP ping
> that tells it each player's address, so a summon times out
> ([deadinside28/bloodborne_pc#121](https://github.com/deadinside28/bloodborne_pc/issues/121));
> the shadPS4 team does not support forks. Messages, bloodstains and ghosts work. For co-op, a
> group runs its own server: [SERVER.md](SERVER.md). *Test the connection* tells which case
> yours is: the shadNet server's UDP line is red on the public server.
## Account
1. On shadPS4's public server, register at [shadnet.shadps4.net](https://shadnet.shadps4.net).
On your own server, its owner creates the account ([SERVER.md](SERVER.md)).
2. In the launcher's **Online** page, turn on **Play online**, choose the **shadNet server**
(shadPS4's public one or your own, 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.
3. **Test the connection**: 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.
## Summoning in the game
- The host rings the **Beckoning Bell** (one Insight each time); the helper rings the **Small
Resonant Bell**.
- 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 ([SERVER.md](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 `UPNP: mapped UDP port <port>`. A router without UPnP needs a port forward by
hand (UDP, the pinned port below, to your computer's LAN address).
2. **Pin the port** when you open it by hand or in a firewall: **Co-op port (UDP)** in the
Online page, for example 3658 (0, the default, is a random port each time). The log then says
`P2P transport ready: UDP physical_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 **Online** page 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
```
On Windows, in a terminal as administrator:
```
netsh advfirewall firewall add rule name="Bloodborne co-op" dir=in action=allow protocol=UDP localport=3658
```
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 |
|---|---|
| `Logged 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. |
| `peer '...' endpoint unresolved` | shadNet has no address for the other player: its UDP port did not answer (the public server, above). |
| `event=0(DEAD)` | The connection gave up. |
+40
View File
@@ -0,0 +1,40 @@
# Documentation
## For players
- [COMPATIBILITY.md](COMPATIBILITY.md): which releases of Bloodborne run, and which have been
checked.
- [MODS.en.md](MODS.en.md): file mods and third-party patches.
- [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 (shadnet-p2p) for a group of friends.
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md): common problems and what to do.
## For this fork
- [RELEASE_TEMPLATE.md](RELEASE_TEMPLATE.md): the format of every release's notes.
## bbport's development notes
Everything else here comes from [bbport](https://github.com/deadinside28/bloodborne_pc) by
deadinside28, kept as it is so that its updates merge cleanly. Many are in Russian (`.ru.md`, or
a Russian title).
- **What changed in each version:** [CHANGES_0.5.md](CHANGES_0.5.md),
[CHANGES_0.5-pre4.md](CHANGES_0.5-pre4.md), [CHANGES_0.5-pre3.md](CHANGES_0.5-pre3.md),
[CHANGES_0.5-pre2.md](CHANGES_0.5-pre2.md), [CHANGES_after_0.4.md](CHANGES_after_0.4.md),
[CHANGES_0.4.md](CHANGES_0.4.md), [CHANGES_2026-10-06.md](CHANGES_2026-10-06.md) (0.3),
[CHANGES_2026-10-03.md](CHANGES_2026-10-03.md), [CHANGES_2026-10-02.md](CHANGES_2026-10-02.md).
- **Where the port is going:** [ROADMAP.md](ROADMAP.md),
[EMULATION_REMOVAL_PLAN.ru.md](EMULATION_REMOVAL_PLAN.ru.md),
[TRANSLATOR_ARCHITECTURE.ru.md](TRANSLATOR_ARCHITECTURE.ru.md).
- **Memory and GPU commands:** [MEMORY_MODULE_PLAN.ru.md](MEMORY_MODULE_PLAN.ru.md),
[NVIDIA_MEMORY_MODEL.ru.md](NVIDIA_MEMORY_MODEL.ru.md),
[TRANSLATION_GPU_COPY_LABELS.ru.md](TRANSLATION_GPU_COPY_LABELS.ru.md),
[parallel_gpu.md](parallel_gpu.md).
- **Upscaling, TAA and motion vectors:** [upscaler.md](upscaler.md),
[DEPTH_ADAPTIVE_TAA.md](DEPTH_ADAPTIVE_TAA.md),
[TEMPORAL_REVIEW_2026-10-01.md](TEMPORAL_REVIEW_2026-10-01.md),
[motion_vectors.md](motion_vectors.md).
- **Mods, patches and TAA sharpness** (the original of MODS.en.md): [MODS.md](MODS.md).
- **The first experiments:** [DEVELOPMENT_LOG.ru.md](DEVELOPMENT_LOG.ru.md), and the session
notes in [history/](history/).
+46
View File
@@ -0,0 +1,46 @@
# Release notes template
Every release uses this format: same sections, same order. A section with nothing to say keeps
its heading and says "None.". Notes are in English. "New", "Improved" and "Fixed" cover the
changes since the previous release. Third-party work is credited in the line that uses it.
- Tag: `<upstream bbport version>-linux.<n>` (e.g. `0.4-linux.1`).
- Title: `bbport Linux <tag>`.
- Asset: `dist/Bloodborne-bbport-x86_64.AppImage` (`bash build.sh && bash packaging/appimage-sharun.sh`).
```markdown
<One or two sentences: what this release is and which upstream bbport it is based on.>
**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
| File | For |
|---|---|
| `Bloodborne-bbport-x86_64.AppImage` | Any x86-64 distribution and the Steam Deck |
Make it executable, run it, choose your game folder and press **Play**. Saves and settings go
to `~/.local/share/bbport`.
SHA-256: `<sha256sum of the AppImage>`
## New
- **<Feature>**: <what it does>.
## Improved
- <Change to something that existed>.
## Fixed
- <Bug fixed>.
## Upgrading
- <What someone coming from the previous release has to do, or "None.">
## Known issues
- <Limitations and known bugs.>
**Full changes:** https://github.com/ayozetr/bloodborne_linux/compare/<previous tag>...<tag>
**Credits and licenses:** https://github.com/ayozetr/bloodborne_linux#credits
```
+180
View File
@@ -0,0 +1,180 @@
# Hosting your own co-op server
Bells and summons go through a shadNet server. Players who connect through shadPS4's public one
(`srv.shadps4.net`) get no co-op at the moment: it does not answer on its UDP port, so a summon
never connects ([deadinside28/bloodborne_pc#121](https://github.com/deadinside28/bloodborne_pc/issues/121)).
A group of friends can run their own server instead:
[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.
Messages, bloodstains and ghosts still come from The Hunter's Dream, whichever shadNet server you
use. 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 the **Online** page:
| 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 **Test the connection**. Every line should be green, the shadNet server's UDP among them: it
shows the address the server sees for you, a `100.` one through Tailscale. A red 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 (experimental)
shadnet-p2p can also summon between different areas: the guest is moved to the host's area. To
use it, the server and every player must have it on:
- The server: add `-e SHADNET_BLOODBORNE_SEAMLESS_COOP=1` to `docker run`, or set
`BloodborneSeamlessCoop=true` in `shadnet.cfg`. Its log then says `seamless co-op enabled`.
- Each player: **Seamless co-op (experimental)** in the launcher's Online page.
Ring the host's Beckoning Bell first, then the guest's Small Resonant Bell. Travelling together
by lantern once the session has started does not work yet.
## 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 is green 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.md](ONLINE.md) for the bells, passwords and log lines of a summon.
+62
View File
@@ -0,0 +1,62 @@
# Troubleshooting
When asking for help, attach the log: in the launcher's **Log** page, **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 [COMPATIBILITY.md](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 starting with `DLSS:`.
- DLSS needs a GeForce RTX (20 series or newer) and NVIDIA's own driver; it does not work with
NVK.
- The 0.4-linux.1 AppImage ships a modified copy of NVIDIA's DLSS library, which the driver
refuses (`Feature requirement query failed: 0xbad00012`): update to 0.5-linux.1 or newer.
- From the repository, DLSS needs `bash tools/build_dlss.sh` once after `build.sh`.
- If the log says `Device creation with DLSS extensions failed`, NVIDIA's CUDA/UVM
module is missing: the game then uses FSR 3.1. Load it with `sudo modprobe nvidia_uvm` (the
`nvidia-modprobe` package loads it when needed); with Secure Boot, a module signing key that
is not enrolled stops it (`Key was rejected by service`).
## Spanish shows the Latin American text (or the other way round)
The game picks a language's regional text by the dump's region. Choose it in **Settings → Game
region**: *Europe* for Spanish from Spain, *The Americas* for Latin American Spanish.
## 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).
## Summons never connect
Run **Test the connection** in the **Online** page. A red line for the shadNet server's UDP means
the server never learns your address, and no summon can connect: shadPS4's public server does
this. Use your own server: [SERVER.md](SERVER.md).
## Other online problems
See [ONLINE.md](ONLINE.md): the account, the bells, the ports and the log lines of a summon.
+3
View File
@@ -168,6 +168,9 @@ target_link_libraries(bbgpu PUBLIC Vulkan::Headers fmt::fmt Boost::headers magic
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(pm4-resync-test EXCLUDE_FROM_ALL ../tests/test_pm4_resync.cpp)
target_compile_options(pm4-resync-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
target_include_directories(pm4-resync-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_link_libraries(motion-shader-test PRIVATE bbgpu)
+3
View File
@@ -30,6 +30,9 @@ 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);
/* 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
@@ -106,6 +106,26 @@ void NpHandler::Initialize() {
{
const auto [host, port] = ParseServerAddress();
const ShadNet::ProbeInfo probe = ShadNet::ProbeServer(host, port);
if (probe.result == ShadNet::ProbeResult::Unreachable) {
// bbport: a server that does not answer yet (still starting, Tailscale not up, a
// short outage) is tried again in the background, as after a dropped connection,
// instead of leaving the game offline for the whole run.
LOG_WARNING(NpHandler,
"shadNet server {}:{} is offline/unreachable; retrying in the background",
host, port);
ImGui::ShadNetNotify::Push(
ImGui::ShadNetNotify::Kind::Info,
fmt::format("shadNet server ({}:{}) is offline. Retrying in the background.",
host, port));
const auto logged_in = UserManagement.GetLoggedInUsers();
for (int i = 0; i < Libraries::UserService::ORBIS_USER_SERVICE_MAX_LOGIN_USERS; ++i) {
if (logged_in[i]) {
MarkForReconnect(logged_in[i]->user_id);
}
}
StartWorker();
return;
}
if (probe.result != ShadNet::ProbeResult::Ok) {
EmulatorSettings.SetShadNetSessionDisabled(true);
switch (probe.result) {
@@ -132,16 +152,7 @@ void NpHandler::Initialize() {
"features are disabled for this session.",
host, port));
break;
default: // Unreachable
LOG_WARNING(NpHandler,
"shadNet server {}:{} is offline/unreachable; disabling shadNet for "
"this run (saved setting unchanged)",
host, port);
ImGui::ShadNetNotify::Push(
ImGui::ShadNetNotify::Kind::Info,
fmt::format("shadNet server ({}:{}) is offline. Online features are disabled "
"for this session.",
host, port));
default:
break;
}
return;
@@ -259,7 +270,12 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons
}
};
const auto note_state = [this, user_id](ShadNet::ShadNetState st) {
std::lock_guard lock(m_mutex_clients);
m_last_connect_state[user_id] = st;
};
const ShadNet::ShadNetState conn_state = client->WaitForConnection();
note_state(conn_state);
if (conn_state != ShadNet::ShadNetState::Ok) {
LOG_ERROR(NpHandler, "user_id={} connection failed (state={})", user_id,
static_cast<int>(conn_state));
@@ -269,6 +285,7 @@ bool NpHandler::ConnectUser(s32 user_id, const std::string& host, u16 port, cons
}
const ShadNet::ShadNetState auth_state = client->WaitForAuthenticated();
note_state(auth_state);
if (auth_state != ShadNet::ShadNetState::Ok) {
LOG_ERROR(NpHandler, "user_id={} authentication failed (state={})", user_id,
static_cast<int>(auth_state));
@@ -329,8 +346,36 @@ void NpHandler::DisconnectUser(s32 user_id) {
}
void NpHandler::OnUserLoggedIn(s32 user_id) {
if (ConnectUserById(user_id))
if (ConnectUserById(user_id)) {
StartWorker();
return;
}
// bbport: a server that does not answer at start (still starting, Tailscale not up yet, a
// short outage) left the game offline for the whole run: the worker that retries dropped
// connections only ran after a first success. It now keeps trying in the background, as
// after a drop, unless retrying cannot help.
if (RetryableFailure(user_id)) {
LOG_WARNING(NpHandler, "user_id={} could not reach shadNet at start; will retry", user_id);
MarkForReconnect(user_id);
StartWorker();
}
}
bool NpHandler::RetryableFailure(s32 user_id) const {
std::lock_guard lock(m_mutex_clients);
const auto it = m_last_connect_state.find(user_id);
if (it == m_last_connect_state.end()) {
return false; // no attempt made: shadNet off, or no account
}
switch (it->second) {
case ShadNet::ShadNetState::FailureResolve:
case ShadNet::ShadNetState::FailureConnect:
case ShadNet::ShadNetState::FailureServerInfo:
case ShadNet::ShadNetState::FailureOther:
return true;
default:
return false;
}
}
void NpHandler::OnUserLoggedOut(s32 user_id) {
@@ -400,10 +445,16 @@ void NpHandler::TryReconnect() {
}
// ConnectUserById locks m_mutex_clients internally; call it unlocked.
const bool ok = ConnectUserById(uid);
const bool retryable = ok || RetryableFailure(uid);
std::lock_guard lock(m_mutex_clients);
if (ok || m_clients.count(uid)) {
m_reconnect.erase(uid);
LOG_INFO(NpHandler, "user_id={} reconnected to shadNet", uid);
} else if (!retryable) {
// The server answered and refused (a password changed meanwhile, another protocol):
// retrying cannot help.
m_reconnect.erase(uid);
LOG_WARNING(NpHandler, "user_id={} shadNet refused the sign-in; no more retries", uid);
} else if (auto it = m_reconnect.find(uid); it != m_reconnect.end()) {
it->second.backoff = std::min(it->second.backoff * 2, kMaxBackoff);
it->second.next_attempt = std::chrono::steady_clock::now() + it->second.backoff;
@@ -303,6 +303,10 @@ private:
std::chrono::milliseconds backoff{0};
};
std::unordered_map<s32, ReconnectState> m_reconnect;
// bbport: how each user's latest connection attempt ended (ConnectUser), so a failure
// retrying cannot fix (wrong name or password, an incompatible server) is not retried.
std::unordered_map<s32, ShadNet::ShadNetState> m_last_connect_state;
bool RetryableFailure(s32 user_id) const;
mutable std::mutex m_mutex_pending_invites;
std::unordered_map<s32, std::vector<PendingInvitation>> m_pending_invites;
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,102 @@
From: bbport
Subject: [PATCH] FSR 4 v07: aspect mask for external image barriers
FfxFsr4VkExternalImageState gets an aspectMask (0 = color). The layout
barriers of a registered external image used VK_IMAGE_ASPECT_COLOR_BIT, which
is invalid for the D32_SFLOAT_S8_UINT depth image (VUID-VkImageMemoryBarrier-
image-03319, subresourceRange-09601).
---
diff --git a/include/ffx_vk_fsr4_v07.h b/include/ffx_vk_fsr4_v07.h
index e856c0c..c698735 100644
--- a/include/ffx_vk_fsr4_v07.h
+++ b/include/ffx_vk_fsr4_v07.h
@@ -61,6 +61,11 @@ typedef struct FfxFsr4VkShaderBlob {
* GENERAL-to-GENERAL record is valid and still establishes the documented
* compute visibility boundary. The record may be updated every frame; it is
* matched by `view` and does not transfer image/view ownership.
+ *
+ * bbport: `aspectMask` names the aspects of `image` that the layout barriers
+ * cover. 0 means VK_IMAGE_ASPECT_COLOR_BIT; a depth/stencil image needs all
+ * of its aspects (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT for
+ * D32_SFLOAT_S8_UINT).
*/
typedef struct FfxFsr4VkExternalImageState {
uint32_t structSize;
@@ -72,6 +77,7 @@ typedef struct FfxFsr4VkExternalImageState {
VkImageLayout restoreLayout;
VkPipelineStageFlags restoreStageMask;
VkAccessFlags restoreAccessMask;
+ VkImageAspectFlags aspectMask;
} FfxFsr4VkExternalImageState;
// FSR4 has: pass0 (pre), pass1-12 (model), pass13 (post), rcas, spd_auto_exposure
diff --git a/src/ffx_vk_fsr4_v07.c b/src/ffx_vk_fsr4_v07.c
index eb42c16..4bb049c 100644
--- a/src/ffx_vk_fsr4_v07.c
+++ b/src/ffx_vk_fsr4_v07.c
@@ -104,6 +104,7 @@ typedef struct {
VkImageLayout restoreLayout;
VkPipelineStageFlags restoreStageMask;
VkAccessFlags restoreAccessMask;
+ VkImageAspectFlags aspectMask; /* external images: aspects the barriers cover */
int externalStateKnown;
int externalTouched;
} VkRes;
@@ -1049,6 +1050,8 @@ static FfxErrorCode cbRegisterRes(FfxInterface* I,
r->restoreLayout = state->restoreLayout;
r->restoreStageMask = state->restoreStageMask;
r->restoreAccessMask = state->restoreAccessMask;
+ r->aspectMask = state->aspectMask ? state->aspectMask
+ : VK_IMAGE_ASPECT_COLOR_BIT;
r->externalStateKnown = 1;
} else if (in->description.type == FFX_RESOURCE_TYPE_BUFFER) {
if (!in->description.size) return FFX_ERROR_INVALID_SIZE;
@@ -1268,7 +1271,8 @@ static FfxErrorCode cbScheduleJob(FfxInterface* I, const FfxGpuJobDescription* j
// ── image layout barrier ──────────────────────────────────────────────────────
-static void imgBarrier(VkCommandBuffer cmd, VkImage img,
+static void imgBarrierAspect(VkCommandBuffer cmd, VkImage img,
+ VkImageAspectFlags aspect,
VkImageLayout from, VkImageLayout to,
VkAccessFlags sa, VkAccessFlags da,
VkPipelineStageFlags ss, VkPipelineStageFlags ds)
@@ -1280,10 +1284,18 @@ static void imgBarrier(VkCommandBuffer cmd, VkImage img,
b.oldLayout=from; b.newLayout=to;
b.srcQueueFamilyIndex=b.dstQueueFamilyIndex=VK_QUEUE_FAMILY_IGNORED;
b.image=img;
- b.subresourceRange=(VkImageSubresourceRange){VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};
+ b.subresourceRange=(VkImageSubresourceRange){aspect,0,1,0,1};
vkCmdPipelineBarrier(cmd,ss,ds,0,0,NULL,0,NULL,1,&b);
}
+static void imgBarrier(VkCommandBuffer cmd, VkImage img,
+ VkImageLayout from, VkImageLayout to,
+ VkAccessFlags sa, VkAccessFlags da,
+ VkPipelineStageFlags ss, VkPipelineStageFlags ds)
+{
+ imgBarrierAspect(cmd, img, VK_IMAGE_ASPECT_COLOR_BIT, from, to, sa, da, ss, ds);
+}
+
static void transitionExternalImagesToCompute(FfxFsr4VkContext* c,
VkCommandBuffer cmd)
{
@@ -1296,7 +1308,7 @@ static void transitionExternalImagesToCompute(FfxFsr4VkContext* c,
? VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT : r->stageMask;
VkAccessFlags srcAccess = r->layout == VK_IMAGE_LAYOUT_UNDEFINED
? 0u : r->accessMask;
- imgBarrier(cmd, r->img, r->layout, VK_IMAGE_LAYOUT_GENERAL,
+ imgBarrierAspect(cmd, r->img, r->aspectMask, r->layout, VK_IMAGE_LAYOUT_GENERAL,
srcAccess, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
srcStage, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
r->layout = VK_IMAGE_LAYOUT_GENERAL;
@@ -1315,7 +1327,7 @@ static FfxErrorCode restoreExternalImages(FfxFsr4VkContext* c,
if (!r->external || r->kind != RES_IMAGE || !r->img ||
!r->externalStateKnown || !r->externalTouched)
continue;
- imgBarrier(cmd, r->img, r->layout, r->restoreLayout,
+ imgBarrierAspect(cmd, r->img, r->aspectMask, r->layout, r->restoreLayout,
r->accessMask, r->restoreAccessMask,
r->stageMask, r->restoreStageMask);
r->layout = r->restoreLayout;
+3
View File
@@ -5,3 +5,6 @@ patches here to its working tree when they are not applied yet:
- `0001-...`: `BB_FSR4_PROFILE` (GPU time per FSR 4 pass) and `BB_FSR4_STATS` (driver
statistics of each pass) in the FSR 4 v07 provider.
- `0002-...`: `aspectMask` in `FfxFsr4VkExternalImageState` (0 = color). The layout barriers
of a registered external image named the color aspect, which is invalid for the
D32_SFLOAT_S8_UINT depth image.
+1 -1
View File
@@ -56,7 +56,7 @@ public:
template <typename Func>
void ForEachItemBelow(TickType tick, Func&& func) {
static constexpr bool RETURNS_BOOL =
std::is_same_v<std::invoke_result<Func, ObjectType>, bool>;
std::is_same_v<std::invoke_result_t<Func, ObjectType>, bool>;
Item* iterator = first_item;
while (iterator) {
if (static_cast<s64>(tick) - static_cast<s64>(iterator->tick) < 0) {
@@ -378,6 +378,29 @@ void VideoOutDriver::Flip(const Request& req) {
// bbport: BB_FRAME_STATS=1 prints flip rate and frame time spread every 5 seconds.
static const bool frame_stats = EmulatorSettingsImpl::Flag("BB_FRAME_STATS", false);
// bbport: without it, one short line every 30 seconds, so a player's last_run.log says how the
// game ran: game frames per second, the slowest 1% of frames, the worst frame and the
// shader/pipeline compiles of the window.
if (!frame_stats) {
using Clock = std::chrono::steady_clock;
static Clock::time_point window_start = Clock::now(), last = window_start;
static std::vector<float> intervals;
const auto now = Clock::now();
intervals.push_back(std::chrono::duration<float, std::milli>(now - last).count());
last = now;
const double window = std::chrono::duration<double>(now - window_start).count();
if (window >= 30.0) {
std::sort(intervals.begin(), intervals.end());
const size_t n = intervals.size();
const double slow_ms = intervals[n - 1 - n / 100];
std::printf("Perf: %.1f FPS over %.0f s, 1%% low %.1f FPS, worst frame %.1f ms, %u "
"shader/pipeline compiles\n",
n / window, window, 1000.0 / std::max(slow_ms, 0.001),
static_cast<double>(intervals.back()), Vulkan::g_bb_compiles.exchange(0));
intervals.clear();
window_start = now;
}
}
if (frame_stats) {
using Clock = std::chrono::steady_clock;
static Clock::time_point window_start = Clock::now(), last = window_start;
+4
View File
@@ -218,6 +218,10 @@ struct Info : InfoPersistent {
std::memcpy(&base, &ud[ptr_index], sizeof(base));
base = reinterpret_cast<const u32*>(VAddr(base) & 0xFFFFFFFFFFFFULL);
}
// bbport: an unbound descriptor table must reach the existing empty-sharp handling.
if (!base) {
return T{};
}
std::memcpy(&data, base + dword_offset, sizeof(T));
return data;
}
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <mutex>
#include <unordered_map>
#include <boost/container/flat_map.hpp>
#include <queue>
@@ -22,6 +23,7 @@
#include "shader_recompiler/ir/reg.h"
#include "shader_recompiler/ir/srt_gvn_table.h"
#include "shader_recompiler/ir/value.h"
#include "bbport_toggles.h"
#ifdef ARCH_X86_64
@@ -30,9 +32,14 @@ using namespace Xbyak::util;
static Xbyak::CodeGenerator g_srt_codegen(32_MB);
static const u8* g_srt_codegen_start = nullptr;
namespace {
void EnsureSrtFaultHandler();
}
namespace Shader {
PFN_SrtWalker RegisterWalkerCode(const u8* ptr, size_t size) {
EnsureSrtFaultHandler();
const auto func_addr = (PFN_SrtWalker)g_srt_codegen.getCurr();
g_srt_codegen.db(ptr, size);
g_srt_codegen.ready();
@@ -77,6 +84,26 @@ static bool SrtWalkerSignalHandler(void* context, void* fault_address) {
return false; // Not in SRT code range
}
// bbport: walks ahead of the game (draw preparation workers, the recording thread's table
// check) may follow pointers into memory the game has reused since. They go back to their
// recovery point (probe.c) and the GPU thread walks the tables itself: patching here made one
// stale read zero that load for the rest of the session. This also keeps a failed check
// below from leaving through a recovery point with the lock held (common/assert.cpp).
if (runtime_fault_recover) {
return false;
}
// bbport: draw-preparation workers run the same walker concurrently. A thread that faulted
// on the instruction before another one patched it finds the patch here and retries; decoding
// it as the original MOV would trip the ASSERT below (exit 23).
static std::mutex patch_mutex;
std::scoped_lock patch_lock{patch_mutex};
const u8* bytes = reinterpret_cast<const u8*>(code);
if ((bytes[0] == 0x48 && bytes[1] == 0x31 && bytes[2] == 0xFF) ||
(bytes[0] == 0x45 && bytes[1] == 0x31 && bytes[2] == 0xD2)) {
return true;
}
// Patch instruction to zero register
ZydisDecodedInstruction instruction;
ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
@@ -90,29 +117,37 @@ static bool SrtWalkerSignalHandler(void* context, void* fault_address) {
size_t len = instruction.length;
const size_t patch_size = 3;
u8* code_patch = const_cast<u8*>(reinterpret_cast<const u8*>(code));
u8 patch[patch_size];
// We can only encounter rdi or r10d as the first operand in a
// fault memory access for SRT walker.
switch (operands[0].reg.value) {
case ZYDIS_REGISTER_RDI:
// mov rdi, [rdi + (off_dw << 2)] -> xor rdi, rdi
code_patch[0] = 0x48;
code_patch[1] = 0x31;
code_patch[2] = 0xFF;
patch[0] = 0x48;
patch[1] = 0x31;
patch[2] = 0xFF;
break;
case ZYDIS_REGISTER_R10D:
// mov r10d, [rdi + (off_dw << 2)] -> xor r10d, r10d
code_patch[0] = 0x45;
code_patch[1] = 0x31;
code_patch[2] = 0xD2;
patch[0] = 0x45;
patch[1] = 0x31;
patch[2] = 0xD2;
break;
default:
UNREACHABLE_MSG("Unsupported register for SRT walker patch");
return false;
}
// Fill nops
memset(code_patch + patch_size, 0x90, len - patch_size);
// bbport: other threads may be running this code. Written from the end, every state in
// between is a load into the same register or a register move into it: nops over the
// displacement, then the ModRM byte (mov reg, reg), the REX prefix, the xor opcode last.
for (size_t i = len; i-- > patch_size;) {
__atomic_store_n(&code_patch[i], u8{0x90}, __ATOMIC_RELEASE);
}
for (const size_t i : {size_t{2}, size_t{0}, size_t{1}}) {
__atomic_store_n(&code_patch[i], patch[i], __ATOMIC_RELEASE);
}
LOG_WARNING(Render_Recompiler, "Patched SRT walker at {}, fault address {}", code,
fault_address);
@@ -120,6 +155,20 @@ static bool SrtWalkerSignalHandler(void* context, void* fault_address) {
return true;
}
// Registers the walker fault handler once, whichever walker is added first (generated here or
// loaded from the pipeline cache through RegisterWalkerCode, which before left it unregistered
// when every walker came from the cache). The buffer is still empty then, so its current
// position is its start.
void EnsureSrtFaultHandler() {
static std::once_flag once;
std::call_once(once, [] {
g_srt_codegen_start = g_srt_codegen.getCurr();
// Call after the memory invalidation handler
constexpr u32 priority = 2;
Core::Signals::Instance()->RegisterAccessViolationHandler(SrtWalkerSignalHandler, priority);
});
}
using namespace Shader;
struct PassInfo {
@@ -641,13 +690,7 @@ static void GenerateSrtProgram(Info& info, PassInfo& pass_info) {
}
// Register the signal handler for SRT walker, if not already registered
if (g_srt_codegen_start == nullptr) {
g_srt_codegen_start = c.getCurr();
auto* signals = Core::Signals::Instance();
// Call after the memory invalidation handler
constexpr u32 priority = 2;
signals->RegisterAccessViolationHandler(SrtWalkerSignalHandler, priority);
}
EnsureSrtFaultHandler();
info.srt_info.walker_func = c.getCurr<PFN_SrtWalker>();
pass_info.dst_off_dw = NUM_USER_DATA_REGS;
+23 -8
View File
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <atomic>
#include <chrono>
#include <thread>
#include <pthread.h>
@@ -34,6 +35,7 @@
#include "bbport_write_log.h"
#include "bbport_free_check.h"
#include "video_core/amdgpu/pm4_cmds.h"
#include "video_core/amdgpu/pm4_resync.h"
#include "video_core/amdgpu/pm4_selftest.h"
#include "cp_decoder.h"
#include "video_core/renderdoc.h"
@@ -1460,14 +1462,27 @@ Liverpool::Task Liverpool::ProcessGraphics(std::span<const u32> dcb, std::span<c
switch (type) {
default:
ReportBadPacket(base_addr, dcb_dwords, dcb.data(), seq, dcb_depth);
UNREACHABLE_MSG("Wrong PM4 type {}", type);
break;
case 0:
ReportBadPacket(base_addr, dcb_dwords, dcb.data(), seq, dcb_depth);
UNREACHABLE_MSG("Unimplemented PM4 type 0, base reg: {}, size: {}",
header->type0.base.Value(), header->type0.NumWords());
break;
case 0: {
// bbport: an invalid header ended the game (UNREACHABLE: "Unimplemented PM4 type
// 0"). Seen at area loads: a SET_SH_REG cut short, whose declared body holds the
// next packets, leaves the decoder inside them. Decoding resumes at the next dword
// that can start a type 3 packet, so the rest of the buffer and its fences still
// run (dropping them would leave the guest waiting for them). The draw preparation
// scanner skips by the same rule (pm4_resync.h).
static std::atomic<int> reports{0};
if (reports.fetch_add(1, std::memory_order_relaxed) < 8) {
ReportBadPacket(base_addr, dcb_dwords, dcb.data(), seq, dcb_depth);
}
const std::size_t skip = ResyncSkip(dcb);
std::fprintf(stderr,
"PM4: invalid header %#x at dword %zu of the buffer, resynced %zu dwords "
"later\n",
header->raw, std::size_t((reinterpret_cast<uintptr_t>(dcb.data()) -
base_addr) / sizeof(u32)),
skip);
dcb = NextPacket(dcb, skip);
continue;
}
case 2:
// Type-2 packet are used for padding purposes
dcb = NextPacket(dcb, 1);
+4 -1
View File
@@ -356,7 +356,10 @@ constexpr NumberConversion MapNumberConversion(const NumberFormat num_fmt,
case DataFormat::Format16_16_16_16:
return NumberConversion::Sint16ToSnormNz;
default:
UNREACHABLE_MSG("data_fmt = {}", u32(data_fmt));
// bbport: was UNREACHABLE. Seen on macOS with data format 45 (FMASK 8 S4 F1) and
// SnormNz, a combination no real resource has (a stale or unused descriptor); it
// ended the game. No conversion is the harmless answer.
return NumberConversion::None;
}
}
default:
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: one rule for stepping over an invalid PM4 packet header.
#pragma once
#include <cstddef>
#include <cstdint>
#include <span>
namespace AmdGpu {
/// A dword that can start a type 3 packet: type 3, reserved bits clear, and the packet within
/// the `remaining` dwords (same bits as PM4Type3Header).
inline bool PlausibleType3(std::uint32_t dword, std::size_t remaining) {
const std::uint32_t num_words = (((dword >> 16) & 0x3fff) + 1) & 0x3fff;
return (dword >> 30) == 3 && (dword & 0xfc) == 0 && num_words < remaining;
}
/// Dwords from the invalid header at the front of `remaining` to the next dword that can start
/// a type 3 packet. Liverpool::ProcessGraphics and the draw preparation scanner both skip by
/// this: when the scanner stopped at the header instead, its register state fell out of step
/// with the GPU thread's for the rest of the session and no prepared draw was used again.
inline std::size_t ResyncSkip(std::span<const std::uint32_t> remaining) {
std::size_t skip = 1;
while (skip < remaining.size() && !PlausibleType3(remaining[skip], remaining.size() - skip)) {
++skip;
}
return skip;
}
} // namespace AmdGpu
@@ -95,9 +95,16 @@ void main() {
// Both depths are full precision. Do not accept an old foreground vector merely
// because perspective depth compresses distant surfaces into a small interval.
float depth_threshold = max(2e-7, (proj.z - depth) * 0.01);
// bbport: an object vector far from the camera's at the same pixel read history that is
// not this object's: still bushes behind the Hunter's Dream workshop got 100-650 px a
// frame while the camera vectors around them were right. Real object motion beyond 5% of
// the frame width per frame (32 px at low render sizes) is rarer than such history, so
// the camera vector stays then.
const float max_object_delta = max(0.05 * size.x, 32.0);
if (object_motion.b > 0.99 && abs(object_motion.a - depth) < depth_threshold &&
!any(isnan(object_motion.rg)) &&
!any(isinf(object_motion.rg))) {
!any(isinf(object_motion.rg)) &&
distance(object_motion.rg, motion) < max_object_delta) {
result = object_motion.rg;
}
}
@@ -4,6 +4,7 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <limits>
#include <span>
#include <unordered_map>
@@ -39,8 +40,22 @@ VertexRange IndexedRange(std::span<const Index, Extent> indices, int32_t base_ve
// Vertex shader constant buffers (stride 16) of Bloodborne's G-buffer shaders: the 864-byte
// scene constants (camera), 416-byte model constants and palettes of 3x4 bone matrices
// (48 bytes per bone). Characters have 656+ bytes; weapons and props 2-8 bones (96-384).
enum class BufferRole { Other, Skeleton, SmallSkeleton };
// bbport: palettes of 16-64 KiB in whole bones (24576, 49152: 512-1024 bones, seen in Central
// Yharnam where the large beasts smeared under FSR) are LargeSkeleton: gated like small ones
// (history only while the palette changes), since instance data of static props may look the
// same. BB_MOTION_LARGE=0 leaves them to camera vectors as before.
enum class BufferRole { Other, Skeleton, SmallSkeleton, LargeSkeleton };
inline bool LargePalettesEnabled() {
static const bool enabled = [] {
const char* env = std::getenv("BB_MOTION_LARGE");
return !env || env[0] != '0';
}();
return enabled;
}
inline BufferRole ClassifyBuffer(uint32_t size) {
if (size > 16384 && size <= 65536 && size % 48 == 0 && LargePalettesEnabled()) {
return BufferRole::LargeSkeleton;
}
if (size == 864 || size > 16384) {
return BufferRole::Other;
}
@@ -14,10 +14,24 @@
// Define assert-on-result to nothing to instead return the result for our handling.
#define VULKAN_HPP_ASSERT_ON_RESULT
// 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 with -UNDEBUG (their asserts must run); the inline init() copies the
// linker keeps would then read the dispatcher at offsets another translation unit did not write
// (a call through a null vkGetInstanceProcAddr). Include it with NDEBUG defined everywhere.
#ifndef NDEBUG
#define NDEBUG
#define BB_VK_COMMON_RESTORE_ASSERTS
#endif
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-value"
#include <vulkan/vulkan.hpp>
#pragma clang diagnostic pop
#ifdef BB_VK_COMMON_RESTORE_ASSERTS
#undef BB_VK_COMMON_RESTORE_ASSERTS
#undef NDEBUG
#include <cassert> // vulkan.hpp's own include of it just turned assert() off
#endif
#define VMA_STATIC_VULKAN_FUNCTIONS 0
#define VMA_DYNAMIC_VULKAN_FUNCTIONS 1
@@ -58,11 +58,15 @@ void* FindSymbol(Module module, const char* name) {
}
#else
// bbport (Linux): libbbport_dlss.so (gpu/dlss_bridge) and NVIDIA's libnvidia-ngx-dlss.so.<version>
// next to bb-probe; NGX finds the latter through the bridge's path list.
// next to bb-probe, or in BB_DLSS_DIR (packaged builds whose /proc/self/exe is the dynamic
// loader, such as an AppImage); NGX finds the latter through the bridge's path list.
constexpr char BridgeName[] = "libbbport_dlss.so";
using Module = void*;
std::filesystem::path ExecutableDirectory() {
if (const char* dir = std::getenv("BB_DLSS_DIR"); dir && dir[0]) {
return dir;
}
std::error_code error;
return std::filesystem::read_symlink("/proc/self/exe", error).parent_path();
}
@@ -256,6 +260,10 @@ void Dlss::AppendDeviceExtensions(vk::Instance instance, vk::PhysicalDevice phys
impl->device_ready = true;
}
void Dlss::Disable(std::string_view reason) {
impl->Disable(reason);
}
void Dlss::Initialize(vk::Instance instance, vk::PhysicalDevice physical, vk::Device device) {
if (!impl->eligible || !impl->device_ready || impl->initialized) {
return;
@@ -343,6 +351,7 @@ Dlss* Dlss::Get() {
void Dlss::AppendInstanceExtensions(std::vector<const char*>&) {}
void Dlss::AppendDeviceExtensions(vk::Instance, vk::PhysicalDevice, std::vector<const char*>&) {}
void Dlss::Initialize(vk::Instance, vk::PhysicalDevice, vk::Device) {}
void Dlss::Disable(std::string_view) {}
void Dlss::Shutdown() {}
bool Dlss::Available() const {
return false;
@@ -12,6 +12,7 @@
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "common/types.h"
@@ -31,6 +32,8 @@ public:
std::vector<const char*>& enabled);
/// After the device exists.
void Initialize(vk::Instance instance, vk::PhysicalDevice physical, vk::Device device);
/// Device creation can fail despite advertised NGX extensions (e.g. missing CUDA/UVM).
void Disable(std::string_view reason);
void Shutdown();
[[nodiscard]] bool Available() const;
@@ -12,6 +12,7 @@
#include "common/thread.h"
#include "video_core/amdgpu/liverpool.h"
#include "video_core/amdgpu/pm4_cmds.h"
#include "video_core/amdgpu/pm4_resync.h"
#include "video_core/renderer_vulkan/vk_draw_prep.h"
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
#include <xxhash.h>
@@ -26,7 +27,8 @@ bool IsDirectDraw(AmdGpu::PM4ItOpcode opcode) {
opcode == PM4ItOpcode::DrawIndexAuto;
}
/// Walks the type-3 packets of a command buffer (type-2 padding skipped).
/// Walks the type-3 packets of a command buffer (type-2 padding skipped, an invalid header
/// stepped over exactly as the GPU thread does, so both register states stay equal).
template <typename Func>
void ForEachPacket(std::span<const u32> commands, Func&& func) {
for (size_t at = 0; at < commands.size();) {
@@ -36,7 +38,8 @@ void ForEachPacket(std::span<const u32> commands, Func&& func) {
continue;
}
if (header->type != 3) {
return;
at += AmdGpu::ResyncSkip(commands.subspan(at));
continue;
}
const size_t words = header->type3.NumWords() + 1;
if (at + words > commands.size()) {
@@ -218,7 +218,8 @@ struct Fsr4Upscaler::Impl {
}
}
VkResult Register(const Image& image, VkAccessFlags access) {
VkResult Register(const Image& image, VkAccessFlags access,
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT) {
const FfxFsr4VkExternalImageState state{
.structSize = sizeof(FfxFsr4VkExternalImageState),
.image = image.image,
@@ -229,6 +230,7 @@ struct Fsr4Upscaler::Impl {
.restoreLayout = VK_IMAGE_LAYOUT_GENERAL,
.restoreStageMask = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
.restoreAccessMask = access,
.aspectMask = aspect,
};
return ffxFsr4VkSetExternalImageState(&backend, &state);
}
@@ -316,15 +318,20 @@ struct Fsr4Upscaler::Impl {
// or every later BeginFrame fails (VK_ERROR_VALIDATION_FAILED_EXT).
const auto abandon = [&] { in_flight.emplace_back(frame_id, scheduler.CurrentTick()); };
constexpr VkAccessFlags read = VK_ACCESS_SHADER_READ_BIT;
const std::array<std::pair<const Image*, VkAccessFlags>, 4> images{{
{&f.color, read},
{&f.depth, read},
{&f.motion, read},
{&f.output, read | VK_ACCESS_SHADER_WRITE_BIT},
// The depth image is D32_SFLOAT_S8_UINT: its barriers name both aspects.
constexpr VkImageAspectFlags color_aspect = VK_IMAGE_ASPECT_COLOR_BIT;
constexpr VkImageAspectFlags depth_aspect =
VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
const std::array<std::tuple<const Image*, VkAccessFlags, VkImageAspectFlags>, 4> images{{
{&f.color, read, color_aspect},
{&f.depth, read, depth_aspect},
{&f.motion, read, color_aspect},
{&f.output, read | VK_ACCESS_SHADER_WRITE_BIT, color_aspect},
}};
static constexpr const char* names[] = {"color", "depth", "motion", "output"};
for (u32 i = 0; i < images.size(); ++i) {
if (const VkResult result = Register(*images[i].first, images[i].second);
if (const VkResult result = Register(*std::get<0>(images[i]), std::get<1>(images[i]),
std::get<2>(images[i]));
result != VK_SUCCESS) {
Fail(std::string{"external image registration failed ("} + names[i] + ", " +
std::to_string(int(result)) + ")",
@@ -31,6 +31,25 @@ struct Skipped {
s64 NowNs() {
return std::chrono::steady_clock::now().time_since_epoch().count();
}
// BB_INDIRECT_GUARD_MAX: the most workgroups one indirect dispatch may launch. A value that is not
// a positive number keeps the default: 0 would make the shader skip every indirect dispatch.
u32 MaxTotal() {
constexpr u32 Default = 1u << 22;
const char* text = std::getenv("BB_INDIRECT_GUARD_MAX");
if (!text || !*text) {
return Default;
}
char* end = nullptr;
const unsigned long long value = std::strtoull(text, &end, 0);
if (*end != '\0' || value == 0 || value > 0xFFFFFFFFull) {
std::printf("Indirect dispatches: BB_INDIRECT_GUARD_MAX=\"%s\" is not a number from 1 to "
"4294967295, keeping the default %u\n",
text, Default);
return Default;
}
return static_cast<u32>(value);
}
} // namespace
bool IndirectGuard::Enabled() {
@@ -57,8 +76,7 @@ IndirectGuard::IndirectGuard(const Instance& instance_, Scheduler& scheduler_)
for (u32 i = 0; i < 3; ++i) {
max_count[i] = limits.maxComputeWorkGroupCount[i];
}
const char* max = std::getenv("BB_INDIRECT_GUARD_MAX");
max_total = max && *max ? static_cast<u32>(std::strtoul(max, nullptr, 0)) : 1u << 22;
max_total = MaxTotal();
std::array<vk::DescriptorSetLayoutBinding, 3> bindings{};
for (u32 i = 0; i < bindings.size(); ++i) {
@@ -196,7 +196,7 @@ Instance::Instance(const Frontend::WindowSystemInfo& window_info, s32 physical_d
VK_VERSION_MAJOR(TargetVulkanApiVersion), VK_VERSION_MINOR(TargetVulkanApiVersion),
VK_VERSION_MAJOR(properties.apiVersion), VK_VERSION_MINOR(properties.apiVersion));
CreateDevice();
ASSERT_MSG(CreateDevice(), "Unable to initialize Vulkan device");
CollectPhysicalMemoryInfo();
CollectImageFormatInfo();
CollectToolingInfo();
@@ -459,6 +459,7 @@ bool Instance::CreateDevice() {
const auto vk11_features = feature_chain.get<vk::PhysicalDeviceVulkan11Features>();
vk12_features = feature_chain.get<vk::PhysicalDeviceVulkan12Features>();
vk13_features = feature_chain.get<vk::PhysicalDeviceVulkan13Features>();
const auto renderer_extension_count = enabled_extensions.size();
// bbport: DLSS (optional bridge library) needs its own device extensions on NVIDIA GPUs.
std::vector<const char*> dlss_extensions;
if (Dlss* dlss = Dlss::Get()) {
@@ -701,6 +702,24 @@ bool Instance::CreateDevice() {
}
auto [device_result, dev] = physical_device.createDeviceUnique(device_chain.get());
if (device_result != vk::Result::eSuccess &&
enabled_extensions.size() > renderer_extension_count) {
// NGX can advertise its extensions even when their runtime dependencies are missing
// (e.g. NVIDIA UVM/CUDA on Linux). DLSS is optional: retry the renderer's original
// extension set, and keep NGX from initializing against a device without its extensions.
LOG_WARNING(Render_Vulkan, "Device creation with DLSS extensions failed: {}; "
"retrying without DLSS", vk::to_string(device_result));
if (Dlss* dlss = Dlss::Get()) {
dlss->Disable(fmt::format("Vulkan device creation failed with DLSS extensions: {} "
"(check NVIDIA CUDA/UVM availability)",
vk::to_string(device_result)));
}
enabled_extensions.resize(renderer_extension_count);
device_chain.get().enabledExtensionCount = static_cast<u32>(enabled_extensions.size());
auto retry = physical_device.createDeviceUnique(device_chain.get());
device_result = retry.result;
dev = std::move(retry.value);
}
if (device_result != vk::Result::eSuccess) {
LOG_CRITICAL(Render_Vulkan, "Failed to create device: {}", vk::to_string(device_result));
return false;
@@ -4,6 +4,7 @@
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
@@ -37,8 +38,70 @@
#include "bbport_guest_memory.h"
#include "bbport_settings.h"
#include "game_profile.h"
#include "bbport_shader_activity.h"
#include "bbport_toggles.h"
// bbport: common/assert.cpp calls it before stopping on a failed check (SpeculativeAssertExit).
extern void (*g_assert_speculative_exit)();
extern "C" int runtime_memory_vma_info(uintptr_t address, int* prot, int* type, uintptr_t* end);
extern "C" const uint64_t* runtime_memory_generation(void);
namespace {
// bbport: a stage's program address points at readable memory. A malformed command buffer (a
// register packet whose values are the next packets) set a shader address of 0x4c0012d0000 and
// reading the program there ended the game; such a draw or dispatch is skipped instead.
template <typename Program>
bool ProgramReadable(const Program& pgm) {
const auto address = reinterpret_cast<uintptr_t>(pgm.template Address<u32*>());
if (!address) {
return false;
}
// Checked for every stage of every draw: the last readable mapping found is remembered for
// as long as the mapping table keeps its generation (an odd one is a change in progress).
thread_local uintptr_t known_start = 0, known_end = 0;
thread_local uint64_t known_generation = 1;
const uint64_t generation = __atomic_load_n(runtime_memory_generation(), __ATOMIC_ACQUIRE);
if (generation == known_generation && address >= known_start && address < known_end) {
return true;
}
int prot = 0, type = -1;
uintptr_t end = 0;
if (!runtime_memory_vma_info(address, &prot, &type, &end)) {
return true; // not a guest mapping (the executable's image)
}
if ((prot & 1) == 0) {
static std::atomic<int> reports{0};
if (reports.fetch_add(1, std::memory_order_relaxed) < 8) {
std::fprintf(stderr, "Pipeline: shader program address %#llx is not readable; the draw "
"is skipped\n", (unsigned long long)address);
}
return false;
}
if ((generation & 1) == 0) {
known_start = address; // the mapping's start is not known: from here on
known_end = end;
known_generation = generation;
}
return true;
}
// bbport: a check that fails while a speculative read has its recovery point armed (draw
// preparation ahead of the GPU thread specializing a shader from descriptors the game may still
// be rewriting) ends that attempt like a fault does, and the GPU thread prepares the draw in
// order. Anywhere else the assertion still stops.
void SpeculativeAssertExit() {
sigjmp_buf* const recover = runtime_fault_recover;
if (!recover) {
return;
}
runtime_fault_recover = nullptr;
std::fputs("GPU: speculative draw preparation abandoned after the check above; the GPU "
"thread prepares this draw in order\n",
stderr);
siglongjmp(*recover, 1);
}
} // namespace
namespace Vulkan {
using Shader::HwStage;
@@ -292,6 +355,7 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_,
: instance{instance_}, scheduler{scheduler_}, liverpool{liverpool_},
desc_heap{instance, scheduler.GetWorkSemaphore(), DescriptorHeapSizes} {
sel.regs = &liverpool->regs;
g_assert_speculative_exit = SpeculativeAssertExit;
const auto& vk12_props = instance.GetVk12Properties();
profile = Shader::Profile{
.max_viewport_width = instance.GetMaxViewportWidth(),
@@ -434,7 +498,11 @@ std::atomic<u32> g_bb_compiles;
namespace {
struct CompileTimer {
std::chrono::steady_clock::time_point start = std::chrono::steady_clock::now();
CompileTimer() {
BbShaderActivity::Begin();
}
~CompileTimer() {
BbShaderActivity::End();
g_bb_compile_ns += u64(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - start)
.count());
@@ -463,7 +531,7 @@ const GraphicsPipeline* PipelineCache::TryPreparedPipeline(const PreparedDraw& p
for (u32 i = 0; i < prepared.num_stages; ++i) {
const auto& stage = prepared.stages[i];
const auto* pgm = regs.ProgramForStage(static_cast<u32>(stage.hw_stage));
if (!pgm || !pgm->Address<u32*>()) {
if (!pgm || !ProgramReadable(*pgm)) {
return nullptr;
}
const auto params = AmdGpu::GetParams(*pgm);
@@ -914,7 +982,7 @@ bool PipelineCache::RefreshGraphicsStages(PipelineSelection& sel) {
}
const auto* pgm = regs.ProgramForStage(stage_in_idx);
if (!pgm || !pgm->Address<u32*>()) {
if (!pgm || !ProgramReadable(*pgm)) {
key.stage_hashes[stage_out_idx] = 0;
sel.infos[stage_out_idx] = nullptr;
return false;
@@ -1025,6 +1093,9 @@ bool PipelineCache::RefreshGraphicsStages(PipelineSelection& sel) {
bool PipelineCache::RefreshComputeKey() {
Shader::Backend::Bindings binding{};
const auto& cs_pgm = liverpool->GetCsRegs();
if (!ProgramReadable(cs_pgm)) {
return false;
}
const auto cs_params = AmdGpu::GetParams(cs_pgm);
std::tie(sel.infos[0], sel.modules[0], sel.fetch_shader, compute_key.value) =
GetProgram(sel, HwStage::Compute, SwStage::Compute, cs_params, binding);
@@ -1094,10 +1165,25 @@ PipelineCache::Result PipelineCache::GetProgram(PipelineSelection& sel, HwStage
return {};
}
runtime_fault_recover = &recover;
info.RefreshFlatBuf();
auto spec = Shader::StageSpecialization(info, runtime_info, profile, binding);
// 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: it is treated like
// a fault. A fault's jump out of the try block does not unwind, so it is unaffected.
std::optional<Shader::StageSpecialization> spec;
try {
info.RefreshFlatBuf();
spec.emplace(info, runtime_info, profile, binding);
} catch (const std::exception& e) {
runtime_fault_recover = nullptr;
static std::atomic<u32> logged{0};
if (logged.fetch_add(1, std::memory_order_relaxed) < 8) {
LOG_WARNING(Render_Vulkan, "Draw preparation ahead of the GPU thread stopped: {}",
e.what());
}
worker.failed = true;
return {};
}
runtime_fault_recover = nullptr;
const auto it = std::ranges::find(program->modules, spec, &Program::Module::spec);
const auto it = std::ranges::find(program->modules, *spec, &Program::Module::spec);
if (it == program->modules.end()) {
worker.failed = true;
return {};
@@ -1701,7 +1701,8 @@ void Rasterizer::DrawRecord(const GraphicsPipeline* pipeline, const PreparedDraw
break;
}
const VAddr address = buffer.base_address;
if (role == Motion::BufferRole::SmallSkeleton &&
if ((role == Motion::BufferRole::SmallSkeleton ||
role == Motion::BufferRole::LargeSkeleton) &&
memory->IsValidGpuMapping(address, 0) &&
memory->ClampRangeSize(address, size) == size) {
palette = XXH3_64bits_withSeed(reinterpret_cast<const void*>(address), size,
@@ -17,6 +17,17 @@
namespace Vulkan {
/// bbport: records `regions` as one command per slice. A deferred command's data shares a
/// recording chunk with it (128 KiB), and a download of the buffer cache can carry far more
/// regions than that (178563 copies, 4.3 MB, loading an area on NVIDIA in 0.5).
template <typename T, typename Record>
static void RecordInSlices(Scheduler& scheduler, std::span<const T> regions, Record&& record) {
constexpr std::size_t PerCommand = 64 * 1024 / sizeof(T);
for (std::size_t at = 0; at < regions.size(); at += PerCommand) {
record(scheduler.RecordData(regions.subspan(at, std::min(PerCommand, regions.size() - at))));
}
}
static vk::ImageType ConvertImageType(AmdGpu::ImageType type) noexcept {
switch (type) {
case AmdGpu::ImageType::Color1D:
@@ -142,9 +153,11 @@ void Runtime::CopyBuffer(const VideoCore::Buffer* src, const VideoCore::Buffer*
FlushBarriers();
}
scheduler.Record([src_handle = src->Handle(), dst_handle = dst->Handle(),
regions = scheduler.RecordData(copies)](vk::CommandBuffer cmdbuf) {
cmdbuf.copyBuffer(src_handle, dst_handle, regions.size(), regions.data());
RecordInSlices(scheduler, copies, [&](std::span<const vk::BufferCopy> regions) {
scheduler.Record([src_handle = src->Handle(), dst_handle = dst->Handle(),
regions](vk::CommandBuffer cmdbuf) {
cmdbuf.copyBuffer(src_handle, dst_handle, regions.size(), regions.data());
});
});
if (bounded) {
@@ -190,9 +203,10 @@ void Runtime::CopyFromGuestChunk(vk::Buffer src, const VideoCore::Buffer* dst,
if (needs_flush) {
FlushBarriers();
}
scheduler.Record([src, dst_handle = dst->Handle(),
regions = scheduler.RecordData(copies)](vk::CommandBuffer cmdbuf) {
cmdbuf.copyBuffer(src, dst_handle, regions.size(), regions.data());
RecordInSlices(scheduler, copies, [&](std::span<const vk::BufferCopy> regions) {
scheduler.Record([src, dst_handle = dst->Handle(), regions](vk::CommandBuffer cmdbuf) {
cmdbuf.copyBuffer(src, dst_handle, regions.size(), regions.data());
});
});
for (const auto& copy : copies) {
AccessBuffer(dst, copy.dstOffset, copy.size, vk::PipelineStageFlagBits2::eCopy,
@@ -286,10 +300,13 @@ void Runtime::UploadImage(VideoCore::Image* dst, const VideoCore::Buffer* src,
FlushBarriers();
}
scheduler.Record([src_handle = src->Handle(), image = dst->GetImage(),
regions = scheduler.RecordData(upload_copies)](vk::CommandBuffer cmdbuf) {
cmdbuf.copyBufferToImage(src_handle, image, vk::ImageLayout::eTransferDstOptimal,
regions.size(), regions.data());
RecordInSlices(scheduler, std::span<const vk::BufferImageCopy>{upload_copies},
[&](std::span<const vk::BufferImageCopy> regions) {
scheduler.Record([src_handle = src->Handle(), image = dst->GetImage(),
regions](vk::CommandBuffer cmdbuf) {
cmdbuf.copyBufferToImage(src_handle, image, vk::ImageLayout::eTransferDstOptimal,
regions.size(), regions.data());
});
});
for (const auto& copy : upload_copies) {
@@ -1471,6 +1471,22 @@ void TemporalUpscaler::OnDraw(u64 vs_hash, VideoCore::ImageId color,
}
return;
}
// bbport: a UI draw without a color target (a Scaleform mask: color writes off) writes the
// stencil that the UI's next draws test. It must go into the UI's output-size depth like
// them: drawn into the guest depth, the mask was lost when the next UI draw switched to the
// UI depth (PrepareUiDepth clears its stencil), and masked images (the loading screen's item
// picture, cut out of a sheet of item pictures) vanished.
if (ui_phase && !color && depth && depth != ui_depth && ui_color) {
const auto& depth_image = texture_cache.GetImage(depth);
const auto& ui_target = texture_cache.GetImage(ui_color);
if (depth_image.info.size.width == ui_target.info.size.width &&
depth_image.info.size.height == ui_target.info.size.height) {
EnsureUiResources(ui_width, ui_height, ui_format, depth_image.info.pixel_format);
PrepareUiDepth(depth);
ui_depth = depth;
}
return;
}
if (!Scaled() || !color) {
return;
}
@@ -181,11 +181,18 @@ void TextureCache::DownloadImageMemory(ImageId image_id, bool sync) {
}
}
/// bbport: the guest memory a MaybeCpuDirty check compares, the same at marking and at
/// refresh (they hashed different ranges, the whole image and its first 8x8 pixels, so the
/// first check never matched). Such an image lies within the faulting page: cheap to hash
/// whole, and a CPU write past its first pixels still counts.
u64 TextureCache::MaybeDirtyHash(const Image& image) {
return XXH3_64bits(std::bit_cast<const u8*>(image.info.guest_address), image.info.guest_size);
}
void TextureCache::MarkAsMaybeDirty(ImageId image_id, Image& image) {
if (image.hash == 0) {
// Initialize hash
const u8* addr = std::bit_cast<u8*>(image.info.guest_address);
image.hash = XXH3_64bits(addr, image.info.guest_size);
image.hash = MaybeDirtyHash(image);
}
image.flags |= ImageFlagBits::MaybeCpuDirty;
UntrackImage(image_id);
@@ -881,17 +888,7 @@ void TextureCache::RefreshImage(Image& image) {
if (True(image.flags & ImageFlagBits::MaybeCpuDirty) &&
False(image.flags & ImageFlagBits::CpuDirty)) {
// The image size should be less than page size to be considered MaybeCpuDirty
// So this calculation should be very uncommon and reasonably fast
// For now we'll just check up to 64 first pixels
const auto addr = std::bit_cast<u8*>(image.info.guest_address);
const u32 w = std::min(image.info.size.width, u32(8));
const u32 h = std::min(image.info.size.height, u32(8));
const u32 s_w = image.info.props.is_block ? Common::DivCeil(w, 4u) : w;
const u32 s_h = image.info.props.is_block ? Common::DivCeil(h, 4u) : h;
const u32 size = s_w * s_h * (image.info.num_bits / 8);
const u64 hash = XXH3_64bits(addr, size);
const u64 hash = MaybeDirtyHash(image);
if (image.hash == hash) {
image.flags &= ~ImageFlagBits::MaybeCpuDirty;
return;
@@ -957,14 +954,18 @@ void TextureCache::RefreshImage(Image& image) {
const auto [mip_size, mip_pitch, mip_height, mip_offset] = image.info.mips_layout[m];
// Protect GPU modified resources from accidental CPU reuploads.
if (is_gpu_modified && !is_gpu_dirty) {
const u8* addr = std::bit_cast<u8*>(image.info.guest_address);
const u64 hash = XXH3_64bits(addr + mip_offset, mip_size);
if (image.mip_hashes[m] == hash) {
continue;
}
image.mip_hashes[m] = hash;
// bbport: every upload records the guest memory it saw, not only uploads of images
// the GPU had already written. An image the GPU writes after an upload from the buffer
// cache (GpuDirty: a storage image's first binding) or from a plain texture otherwise
// had no reference, and the first CPU write anywhere in its page replaced the GPU's
// contents with stale guest memory (a 1x1 exposure texture computed once: the
// character creation preview went black after one frame).
const u8* mip_addr = std::bit_cast<u8*>(image.info.guest_address) + mip_offset;
const u64 mip_hash = XXH3_64bits(mip_addr, mip_size);
if (is_gpu_modified && !is_gpu_dirty && image.mip_hashes[m] == mip_hash) {
continue;
}
image.mip_hashes[m] = mip_hash;
const u32 extent_width = mip_pitch ? std::min(mip_pitch, width) : width;
const u32 extent_height = mip_height ? std::min(mip_height, height) : height;
@@ -380,6 +380,7 @@ private:
void UntrackImageTail(ImageId image_id);
void MarkAsMaybeDirty(ImageId image_id, Image& image);
static u64 MaybeDirtyHash(const Image& image);
/// Removes the image and any views/surface metas that reference it.
void DeleteImage(ImageId image_id);
+22
View File
@@ -15,6 +15,7 @@
#include <boost/asio/io_context.hpp>
#include "common/polyfill_thread.h"
#include "video_core/renderdoc.h"
#include <cstdio>
#include <cstring>
#include <mutex>
#include <string>
@@ -398,6 +399,27 @@ extern "C" int bbgpu_overlay_captures_input(void) {
return BbOverlay::CapturesInput() ? 1 : 0;
}
extern "C" void bbgpu_trophy_notice(int id, char grade, const char* name_utf8, const char* icon_path) {
std::vector<u8> icon;
int size = 0;
if (icon_path != nullptr) {
if (std::FILE* file = std::fopen(icon_path, "rb")) {
u32 header[2]{};
if (std::fread(header, sizeof(header), 1, file) == 1 && header[0] == header[1] &&
header[0] > 0 && header[0] <= 512) {
icon.resize(size_t(header[0]) * header[0] * 4);
if (std::fread(icon.data(), 1, icon.size(), file) == icon.size()) {
size = int(header[0]);
} else {
icon.clear();
}
}
std::fclose(file);
}
}
BbOverlay::TrophyNotice(id, grade, name_utf8 ? name_utf8 : "", std::move(icon), size);
}
extern "C" void bbgpu_mouse_take(double* dx, double* dy, int* wheel_up, int* wheel_down) {
double x = 0, y = 0;
int up = 0, down = 0;
+45 -12
View File
@@ -54,8 +54,9 @@ F Game(u64 va) {
// ---- Texts: ids from IdBase up, looked up by the game like its own messages. ----
constexpr u32 IdBase = 0x7f0000;
/// A text in every menu language, as the game's UTF-16.
struct Text {
std::u16string english, russian;
std::array<std::u16string, int(BbSettings::MenuLanguage::Count)> languages;
};
std::vector<Text> texts;
@@ -84,21 +85,53 @@ std::u16string Utf16(const char* utf8) {
return out;
}
u32 AddText(const char* english, const char* russian) {
texts.push_back({Utf16(english), Utf16(russian)});
Text text;
for (int l = 0; l < int(BbSettings::MenuLanguage::Count); ++l) {
// The game's font has no Chinese characters: its pages stay in English there.
const auto language = BbSettings::MenuLanguage(l) == BbSettings::MenuLanguage::ChineseSimplified
? BbSettings::MenuLanguage::English
: BbSettings::MenuLanguage(l);
text.languages[l] = Utf16(BbSettings::MenuTextIn(language, english, russian));
}
texts.push_back(std::move(text));
return IdBase + u32(texts.size() - 1);
}
/// The game's own language decides (its "Screen/Sound" text in Cyrillic: Russian, else English);
/// the port's menu language until the game has looked that text up.
enum class GameLanguage { Unknown, English, Russian };
std::atomic<GameLanguage> game_language{GameLanguage::Unknown};
/// The game's language as the runtime gives it to the game (SystemService: BB_LANGUAGE, else the
/// desktop's locale), as a menu language; English for the languages the menu does not have.
BbSettings::MenuLanguage LanguageOfTheGame() {
using BbSettings::MenuLanguage;
int id = 1;
if (const char* v = std::getenv("BB_LANGUAGE"); v && *v) {
id = std::atoi(v);
} else {
for (const char* key : {"LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"}) {
if (const char* locale = std::getenv(key); locale && *locale) {
const MenuLanguage l = BbSettings::MenuLanguageFromCode(locale);
return l == MenuLanguage::PortugueseBrazil || l == MenuLanguage::ChineseSimplified
? MenuLanguage::English
: l;
}
}
}
switch (id) {
case 2: return MenuLanguage::French;
case 3: return MenuLanguage::Spanish;
case 4: return MenuLanguage::German;
case 5: return MenuLanguage::Italian;
case 8: return MenuLanguage::Russian;
default: return MenuLanguage::English;
}
}
/// The game's own language decides (its "Screen/Sound" text in Cyrillic: Russian, else the
/// language the runtime gave the game); the port's menu language until the game has looked that
/// text up. -1: not known yet.
std::atomic<int> game_language{-1};
const char16_t* TextOf(u32 id) {
const Text& t = texts[id - IdBase];
const GameLanguage game = game_language.load();
const bool russian = game == GameLanguage::Unknown
? BbSettings::Get().menu_language == BbSettings::MenuLanguage::Russian
: game == GameLanguage::Russian;
return russian ? t.russian.c_str() : t.english.c_str();
const int game = game_language.load();
const int language = game < 0 ? int(BbSettings::Get().menu_language.load()) : game;
return t.languages[language].c_str();
}
// ---- The port's values the game's rows edit (ints: the list rows bind ints). ----
@@ -463,7 +496,7 @@ const char16_t* LookupHook(void* repository, u32 table, u32 category, u32 id) {
for (const char16_t* c = text; *c; ++c) {
cyrillic = cyrillic || (*c >= 0x400 && *c < 0x500);
}
game_language = cyrillic ? GameLanguage::Russian : GameLanguage::English;
game_language = int(cyrillic ? BbSettings::MenuLanguage::Russian : LanguageOfTheGame());
}
return text;
}
+898
View File
@@ -0,0 +1,898 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Overlay translations other than Russian (which sits next to the English in each MenuText call):
// English text -> Spanish, Italian, French, German, Brazilian Portuguese, Simplified Chinese.
// Only bbport_settings.cpp includes this.
#pragma once
#include <array>
#include <string_view>
#include <unordered_map>
#include "bbport_settings.h"
namespace BbSettings {
/// The English text in another MenuLanguage (Spanish onwards); the English when missing.
inline const char* MenuTranslation(MenuLanguage language, const char* english) {
static constexpr int First = int(MenuLanguage::Spanish);
using Row = std::array<const char*, int(MenuLanguage::Count) - First>;
static const std::unordered_map<std::string_view, Row> table{
{"Bloodborne - Graphics (Insert / L3+R3)###bbport_settings",
{"Bloodborne - Gráficos (Insert / L3+R3)###bbport_settings",
"Bloodborne - Grafica (Insert / L3+R3)###bbport_settings",
"Bloodborne - Graphismes (Insert / L3+R3)###bbport_settings",
"Bloodborne - Grafik (Insert / L3+R3)###bbport_settings",
"Bloodborne - Gráficos (Insert / L3+R3)###bbport_settings",
"Bloodborne - 图形 (Insert / L3+R3)###bbport_settings"}},
{"You have earned a trophy.",
{"Has conseguido un trofeo.",
"Hai ottenuto un trofeo.",
"Vous avez obtenu un trophée.",
"Du hast eine Trophäe erhalten.",
"Você ganhou um troféu.",
"你获得了一个奖杯。"}},
{"%.0f FPS (%.1f ms)",
{"%.0f FPS (%.1f ms)",
"%.0f FPS (%.1f ms)",
"%.0f FPS (%.1f ms)",
"%.0f FPS (%.1f ms)",
"%.0f FPS (%.1f ms)",
"%.0f FPS (%.1f 毫秒)"}},
{"Temporal upscaler",
{"Escalador temporal",
"Upscaler temporale",
"Upscaler temporel",
"Temporaler Upscaler",
"Upscaler temporal",
"时间性超分辨率"}},
{"Off",
{"Desactivado",
"Disattivato",
"Désactivé",
"Aus",
"Desativado",
"关闭"}},
{"TAA (native anti-aliasing)",
{"TAA (antialiasing nativo)",
"TAA (anti-aliasing nativo)",
"TAA (anticrénelage natif)",
"TAA (native Kantenglättung)",
"TAA (anti-aliasing nativo)",
"TAA(原生抗锯齿)"}},
{"Upscaler",
{"Escalador",
"Upscaler",
"Upscaler",
"Upscaler",
"Upscaler",
"超分辨率"}},
{"— not supported by this GPU",
{"— no compatible con esta GPU",
"— non supportato da questa GPU",
"— non pris en charge par ce GPU",
"— von dieser GPU nicht unterstützt",
"— não suportado por esta GPU",
"— 此显卡不支持"}},
{"— in development",
{"— en desarrollo",
"— in sviluppo",
"— en développement",
"— in Entwicklung",
"— em desenvolvimento",
"— 开发中"}},
{"FSR 4 unavailable: %s",
{"FSR 4 no disponible: %s",
"FSR 4 non disponibile: %s",
"FSR 4 indisponible : %s",
"FSR 4 nicht verfügbar: %s",
"FSR 4 indisponível: %s",
"FSR 4 不可用:%s"}},
{"The mode selected above is active. You can select FSR 4 again.",
{"El modo seleccionado arriba está activo. Puedes volver a elegir FSR 4.",
"La modalità selezionata sopra è attiva. Puoi selezionare di nuovo FSR 4.",
"Le mode sélectionné ci-dessus est actif. Vous pouvez sélectionner FSR 4 à nouveau.",
"Der oben gewählte Modus ist aktiv. Du kannst FSR 4 erneut auswählen.",
"O modo selecionado acima está ativo. Você pode selecionar o FSR 4 novamente.",
"已启用上方所选的模式。你可以再次选择 FSR 4。"}},
{"FSR 4.1.1: the model from AMD's 4.1.1 DLL, reproduced in Vulkan (output matches the DLL). Assets are built from your DLL in the launcher.",
{"FSR 4.1.1: el modelo de la DLL 4.1.1 de AMD, reproducido en Vulkan (la salida coincide con la DLL). Los recursos se crean a partir de tu DLL en el launcher.",
"FSR 4.1.1: il modello della DLL 4.1.1 di AMD, riprodotto in Vulkan (l'uscita coincide con la DLL). Le risorse si creano dalla tua DLL nel launcher.",
"FSR 4.1.1 : le modèle de la DLL 4.1.1 d'AMD, reproduit en Vulkan (la sortie correspond à la DLL). Les ressources se créent à partir de votre DLL dans le launcher.",
"FSR 4.1.1: das Modell aus AMDs 4.1.1-DLL, in Vulkan nachgebildet (die Ausgabe stimmt mit der DLL überein). Die Assets werden im Launcher aus deiner DLL erstellt.",
"FSR 4.1.1: o modelo da DLL 4.1.1 da AMD, reproduzido em Vulkan (a saída coincide com a DLL). Os recursos são criados a partir da sua DLL no launcher.",
"FSR 4.1.1:AMD 4.1.1 DLL 中的模型,在 Vulkan 中复现(输出与 DLL 一致)。资源在启动器中由你的 DLL 构建。"}},
{"FSR 4 in INT8 mode (v07 model from AMD FidelityFX SDK sources). Higher quality than FSR 3.1, but the pass is more demanding. Changing the preset rebuilds the model (a brief pause).",
{"FSR 4 en modo INT8 (modelo v07 de las fuentes del FidelityFX SDK de AMD). Mayor calidad que FSR 3.1, pero la pasada es más exigente. Al cambiar el preajuste se reconstruye el modelo (una breve pausa).",
"FSR 4 in modalità INT8 (modello v07 dai sorgenti dell'SDK FidelityFX di AMD). Qualità superiore a FSR 3.1, ma il passaggio è più pesante. Cambiare il preset ricostruisce il modello (una breve pausa).",
"FSR 4 en mode INT8 (modèle v07 issu des sources du SDK FidelityFX d'AMD). Meilleure qualité que FSR 3.1, mais la passe est plus exigeante. Changer le préréglage reconstruit le modèle (une courte pause).",
"FSR 4 im INT8-Modus (v07-Modell aus den Quellen von AMDs FidelityFX SDK). Höhere Qualität als FSR 3.1, aber der Durchgang ist anspruchsvoller. Ein Wechsel der Voreinstellung baut das Modell neu auf (eine kurze Pause).",
"FSR 4 no modo INT8 (modelo v07 das fontes do FidelityFX SDK da AMD). Qualidade maior que o FSR 3.1, mas a passada é mais exigente. Mudar a predefinição reconstrói o modelo (uma breve pausa).",
"INT8 模式的 FSR 4(来自 AMD FidelityFX SDK 源码的 v07 模型)。画质高于 FSR 3.1,但这一通道开销更大。更改预设会重建模型(短暂停顿)。"}},
{"FSR 4: auto exposure",
{"FSR 4: exposición automática",
"FSR 4: esposizione automatica",
"FSR 4 : exposition automatique",
"FSR 4: Auto-Belichtung",
"FSR 4: exposição automática",
"FSR 4:自动曝光"}},
{"FSR 4: invert jitter sign",
{"FSR 4: invertir signo del jitter",
"FSR 4: inverti segno del jitter",
"FSR 4 : inverser le signe du jitter",
"FSR 4: Jitter-Vorzeichen invertieren",
"FSR 4: inverter o sinal do jitter",
"FSR 4:反转抖动符号"}},
{"For diagnosing ghosting: the FSR 4 network normalizes color by exposure and uses it to decide when to discard previous frames. Changes apply immediately, without a restart.",
{"Para diagnosticar ghosting: la red de FSR 4 normaliza el color según la exposición y la usa para decidir cuándo descartar fotogramas anteriores. Los cambios se aplican al instante, sin reiniciar.",
"Per diagnosticare il ghosting: la rete FSR 4 normalizza il colore in base all'esposizione e la usa per decidere quando scartare i fotogrammi precedenti. Le modifiche si applicano subito, senza riavvio.",
"Pour diagnostiquer le ghosting : le réseau FSR 4 normalise la couleur selon l'exposition et s'en sert pour décider quand écarter les images précédentes. Les changements s'appliquent immédiatement, sans redémarrage.",
"Zur Diagnose von Geisterbildern: Das FSR-4-Netz normalisiert die Farbe anhand der Belichtung und entscheidet damit, wann vorherige Frames verworfen werden. Änderungen gelten sofort, ohne Neustart.",
"Para diagnosticar ghosting: a rede do FSR 4 normaliza a cor pela exposição e a usa para decidir quando descartar quadros anteriores. As mudanças valem na hora, sem reiniciar.",
"用于诊断拖影:FSR 4 网络按曝光对颜色进行归一化,并据此决定何时丢弃之前的帧。更改立即生效,无需重启。"}},
{"Preset",
{"Preajuste",
"Preset",
"Préréglage",
"Voreinstellung",
"Predefinição",
"预设"}},
{"%s (x%.1f, render %dx%d)",
{"%s (x%.1f, render %dx%d)",
"%s (x%.1f, render %dx%d)",
"%s (x%.1f, rendu %dx%d)",
"%s (x%.1f, Render %dx%d)",
"%s (x%.1f, render %dx%d)",
"%s(x%.1f,渲染 %dx%d)"}},
{"TAA anti-aliases the scene at output resolution, without an FSR model or upscaling. Your saved FSR preset is restored when you select FSR.",
{"TAA aplica antialiasing a la escena en la resolución de salida, sin modelo de FSR ni escalado. Tu preajuste de FSR guardado se restaura al elegir FSR.",
"Il TAA applica l'anti-aliasing alla scena alla risoluzione di uscita, senza modello FSR né upscaling. Il preset FSR salvato viene ripristinato quando selezioni FSR.",
"Le TAA applique l'anticrénelage à la scène à la résolution de sortie, sans modèle FSR ni upscaling. Votre préréglage FSR enregistré est restauré quand vous sélectionnez FSR.",
"TAA glättet die Szene in Ausgabeauflösung, ohne FSR-Modell und ohne Upscaling. Deine gespeicherte FSR-Voreinstellung wird wiederhergestellt, sobald du FSR wählst.",
"O TAA aplica anti-aliasing à cena na resolução de saída, sem modelo do FSR nem upscaling. Sua predefinição salva do FSR volta ao selecionar o FSR.",
"TAA 以输出分辨率对场景进行抗锯齿,不使用 FSR 模型,也不放大。选择 FSR 时会恢复你保存的 FSR 预设。"}},
{"Active scene render: %d x %d",
{"Render activo de la escena: %d x %d",
"Render attivo della scena: %d x %d",
"Rendu actif de la scène : %d x %d",
"Aktives Szenen-Rendering: %d x %d",
"Render atual da cena: %d x %d",
"当前场景渲染:%d x %d"}},
{"Startup preset: %s",
{"Preajuste de inicio: %s",
"Preset all'avvio: %s",
"Préréglage au démarrage : %s",
"Startvoreinstellung: %s",
"Predefinição de início: %s",
"启动时的预设:%s"}},
{"When output is not 1080p, the entire game renders at the preset resolution (startup patch): this is fastest on Steam Deck and weaker GPUs. Preset or output resolution changes require a restart. Enable Live resolution changes below to change them without restarting (post-processing then stays at 1080p, which is slower).",
{"Si la salida no es 1080p, todo el juego se renderiza en la resolución del preajuste (parche de inicio): es lo más rápido en la Steam Deck y en GPU modestas. Los cambios de preajuste o de resolución de salida requieren reiniciar. Activa Cambio de resolución en tiempo real más abajo para cambiarlos sin reiniciar (el posprocesado se queda entonces en 1080p, que es más lento).",
"Se l'uscita non è 1080p, tutto il gioco viene renderizzato alla risoluzione del preset (patch all'avvio): è la scelta più veloce su Steam Deck e GPU meno potenti. Per cambiare preset o risoluzione di uscita serve riavviare. Attiva Cambio di risoluzione in tempo reale qui sotto per cambiarli senza riavviare (la post-elaborazione resta però a 1080p, che è più lento).",
"Quand la sortie n'est pas en 1080p, tout le jeu est rendu à la résolution du préréglage (patch de démarrage) : c'est le plus rapide sur Steam Deck et les GPU modestes. Changer de préréglage ou de résolution de sortie nécessite un redémarrage. Activez Changement de résolution en direct ci-dessous pour les changer sans redémarrer (le post-traitement reste alors en 1080p, ce qui est plus lent).",
"Ist die Ausgabe nicht 1080p, rendert das ganze Spiel in der Auflösung der Voreinstellung (Startpatch): Das ist am schnellsten auf dem Steam Deck und schwächeren GPUs. Änderungen an Voreinstellung oder Ausgabeauflösung erfordern einen Neustart. Aktiviere unten Live-Auflösungswechsel, um sie ohne Neustart zu ändern (die Nachbearbeitung bleibt dann bei 1080p, was langsamer ist).",
"Quando a saída não é 1080p, o jogo inteiro é renderizado na resolução da predefinição (patch de início): é o mais rápido no Steam Deck e em GPUs mais fracas. Mudanças de predefinição ou de resolução de saída exigem reiniciar. Ative Mudança de resolução em tempo real abaixo para mudá-las sem reiniciar (o pós-processamento fica então em 1080p, o que é mais lento).",
"输出不是 1080p 时,整个游戏都按预设分辨率渲染(启动补丁):这在 Steam Deck 和较弱的显卡上最快。更改预设或输出分辨率需要重启。在下方开启“实时更改分辨率”即可无需重启(后期处理则保持 1080p,速度更慢)。"}},
{"BB_RENDER_RES fixes the scene size at startup. Remove this explicit environment variable to change resolution and presets without restarting the game.",
{"BB_RENDER_RES fija el tamaño de la escena al iniciar. Elimina esta variable de entorno explícita para cambiar la resolución y los preajustes sin reiniciar el juego.",
"BB_RENDER_RES fissa la dimensione della scena all'avvio. Rimuovi questa variabile d'ambiente esplicita per cambiare risoluzione e preset senza riavviare il gioco.",
"BB_RENDER_RES fixe la taille de la scène au démarrage. Supprimez cette variable d'environnement explicite pour changer la résolution et les préréglages sans redémarrer le jeu.",
"BB_RENDER_RES legt die Szenengröße beim Start fest. Entferne diese explizite Umgebungsvariable, um Auflösung und Voreinstellungen ohne Neustart des Spiels zu ändern.",
"BB_RENDER_RES fixa o tamanho da cena no início. Remova essa variável de ambiente explícita para mudar a resolução e as predefinições sem reiniciar o jogo.",
"BB_RENDER_RES 会在启动时固定场景尺寸。移除这个显式设置的环境变量,即可无需重启游戏更改分辨率和预设。"}},
{"Native AA: the upscaler acts as anti-aliasing. Other presets reduce the scene render resolution relative to the output. The UI renders at output resolution. The preset applies from the next frame without restarting the game.",
{"Native AA: el escalador actúa como antialiasing. Los demás preajustes reducen la resolución de render de la escena respecto a la salida. La interfaz se renderiza en la resolución de salida. El preajuste se aplica desde el siguiente fotograma, sin reiniciar el juego.",
"Native AA: l'upscaler agisce come anti-aliasing. Gli altri preset riducono la risoluzione di render della scena rispetto all'uscita. L'interfaccia viene renderizzata alla risoluzione di uscita. Il preset si applica dal fotogramma successivo, senza riavviare il gioco.",
"Native AA : l'upscaler sert d'anticrénelage. Les autres préréglages réduisent la résolution de rendu de la scène par rapport à la sortie. L'interface est rendue à la résolution de sortie. Le préréglage s'applique dès l'image suivante, sans redémarrer le jeu.",
"Native AA: der Upscaler dient als Kantenglättung. Andere Voreinstellungen verringern die Render-Auflösung der Szene relativ zur Ausgabe. Die UI rendert in Ausgabeauflösung. Die Voreinstellung gilt ab dem nächsten Frame, ohne Neustart des Spiels.",
"Native AA: o upscaler atua como anti-aliasing. As outras predefinições reduzem a resolução de render da cena em relação à saída. A interface é renderizada na resolução de saída. A predefinição vale a partir do próximo quadro, sem reiniciar o jogo.",
"Native AA:超分辨率充当抗锯齿。其他预设会相对输出降低场景的渲染分辨率。界面按输出分辨率渲染。预设从下一帧起生效,无需重启游戏。"}},
{"Sharpening (RCAS)",
{"Nitidez (RCAS)",
"Nitidezza (RCAS)",
"Netteté (RCAS)",
"Schärfen (RCAS)",
"Nitidez (RCAS)",
"锐化(RCAS)"}},
{"Sharpness",
{"Nitidez",
"Nitidezza",
"Netteté",
"Schärfe",
"Nitidez",
"锐度"}},
{"Up to 1: the upscaler's own sharpening (RCAS). Above 1 adds another RCAS pass. Ctrl+click the slider to enter an exact value.",
{"Hasta 1: el afilado propio del escalador (RCAS). Por encima de 1 se añade otra pasada de RCAS. Ctrl+clic en el control deslizante para escribir un valor exacto.",
"Fino a 1: la nitidezza propria dell'upscaler (RCAS). Oltre 1 aggiunge un altro passaggio RCAS. Ctrl+clic sul cursore per inserire un valore esatto.",
"Jusqu'à 1 : la netteté propre à l'upscaler (RCAS). Au-delà de 1, une passe RCAS supplémentaire s'ajoute. Ctrl+clic sur le curseur pour saisir une valeur exacte.",
"Bis 1: die eigene Schärfung des Upscalers (RCAS). Über 1 kommt ein weiterer RCAS-Durchlauf hinzu. Strg+Klick auf den Regler, um einen genauen Wert einzugeben.",
"Até 1: a nitidez do próprio upscaler (RCAS). Acima de 1 é adicionada outra passada de RCAS. Ctrl+clique no controle deslizante para digitar um valor exato.",
"不超过 1:超分辨率自带的锐化(RCAS)。超过 1 会再加一次 RCAS。按住 Ctrl 点击滑块可输入精确数值。"}},
{"Subpixel jitter",
{"Jitter subpíxel",
"Jitter subpixel",
"Jitter subpixel",
"Subpixel-Jitter",
"Jitter de subpixel",
"子像素抖动"}},
{"Each frame shifts the scene by a fraction of a pixel, letting the upscaler reconstruct more detail from multiple frames. Without it, only history-based anti-aliasing remains.",
{"En cada fotograma la escena se desplaza una fracción de píxel, lo que permite al escalador reconstruir más detalle a partir de varios fotogramas. Sin él, solo queda el antialiasing basado en el historial.",
"A ogni fotogramma la scena si sposta di una frazione di pixel, così l'upscaler ricostruisce più dettagli da più fotogrammi. Senza, resta solo l'anti-aliasing basato sulla cronologia.",
"À chaque image, la scène est décalée d'une fraction de pixel, ce qui permet à l'upscaler de reconstruire plus de détails à partir de plusieurs images. Sans lui, il ne reste que l'anticrénelage basé sur l'historique.",
"Jeder Frame verschiebt die Szene um einen Bruchteil eines Pixels, sodass der Upscaler aus mehreren Frames mehr Details rekonstruieren kann. Ohne Jitter bleibt nur historienbasierte Kantenglättung.",
"A cada quadro a cena se desloca uma fração de pixel, o que permite ao upscaler reconstruir mais detalhes a partir de vários quadros. Sem ele, resta apenas o anti-aliasing baseado no histórico.",
"每帧将场景偏移一个像素的几分之一,让超分辨率能从多帧中重建更多细节。关闭后只剩基于历史帧的抗锯齿。"}},
{"Reactive mask",
{"Máscara reactiva",
"Maschera reattiva",
"Masque réactif",
"Reaktive Maske",
"Máscara reativa",
"反应遮罩"}},
{"Enable mask",
{"Activar máscara",
"Attiva maschera",
"Activer le masque",
"Maske aktivieren",
"Ativar máscara",
"启用遮罩"}},
{"Marks transparent effects (haze, light, water, particles). FSR 4 takes no mask itself: its output is blended with the current frame there, so the effects do not drag previous frames along.",
{"Marca los efectos transparentes (neblina, luz, agua, partículas). FSR 4 no admite máscara: en esas zonas su salida se mezcla con el fotograma actual, para que los efectos no arrastren fotogramas anteriores.",
"Segna gli effetti trasparenti (foschia, luce, acqua, particelle). FSR 4 non usa una maschera: lì il suo output viene fuso con il fotogramma corrente, così gli effetti non trascinano i fotogrammi precedenti.",
"Marque les effets transparents (brume, lumière, eau, particules). FSR 4 n'utilise pas de masque : sa sortie est mélangée avec l'image actuelle à ces endroits, pour que les effets n'entraînent pas les images précédentes.",
"Markiert transparente Effekte (Dunst, Licht, Wasser, Partikel). FSR 4 nimmt selbst keine Maske: Dort wird seine Ausgabe mit dem aktuellen Frame gemischt, damit die Effekte keine früheren Frames mitziehen.",
"Marca os efeitos transparentes (névoa, luz, água, partículas). O FSR 4 não aceita máscara: nessas áreas sua saída é misturada ao quadro atual, para que os efeitos não arrastem quadros anteriores.",
"标记透明效果(雾气、光线、水、粒子)。FSR 4 本身不使用遮罩:这些位置的输出会与当前帧混合,使效果不会拖带之前的帧。"}},
{"Marks transparent effects (particles, haze) so the upscaler relies less on previous frames. Reduces trails behind effects, but shimmering returns underneath them.",
{"Marca los efectos transparentes (partículas, neblina) para que el escalador dependa menos de los fotogramas anteriores. Reduce las estelas tras los efectos, pero vuelve el centelleo bajo ellos.",
"Segna gli effetti trasparenti (particelle, foschia) così l'upscaler si affida meno ai fotogrammi precedenti. Riduce le scie dietro gli effetti, ma sotto di essi torna lo sfarfallio.",
"Marque les effets transparents (particules, brume) pour que l'upscaler s'appuie moins sur les images précédentes. Réduit les traînées derrière les effets, mais le scintillement revient en dessous.",
"Markiert transparente Effekte (Partikel, Dunst), damit sich der Upscaler weniger auf frühere Frames stützt. Verringert Schlieren hinter Effekten, aber darunter kehrt das Flimmern zurück.",
"Marca os efeitos transparentes (partículas, névoa) para que o upscaler dependa menos dos quadros anteriores. Reduz os rastros atrás dos efeitos, mas a cintilação volta embaixo deles.",
"标记透明效果(粒子、雾气),让超分辨率更少依赖之前的帧。可减少效果后的拖尾,但其下方的闪烁会重新出现。"}},
{"Scale",
{"Escala",
"Scala",
"Échelle",
"Skalierung",
"Escala",
"缩放"}},
{"Threshold",
{"Umbral",
"Soglia",
"Seuil",
"Schwellenwert",
"Limiar",
"阈值"}},
{"Maximum",
{"Máximo",
"Massimo",
"Maximum",
"Maximum",
"Máximo",
"最大值"}},
{"Show mask (debug)",
{"Mostrar máscara (depuración)",
"Mostra maschera (debug)",
"Afficher le masque (debug)",
"Maske anzeigen (Debug)",
"Mostrar máscara (depuração)",
"显示遮罩(调试)"}},
{"Character motion vectors",
{"Vectores de movimiento de personajes",
"Vettori di movimento dei personaggi",
"Vecteurs de mouvement des personnages",
"Bewegungsvektoren von Figuren",
"Vetores de movimento dos personagens",
"角色运动矢量"}},
{"Accurate vectors for animated objects: clothing and weapons break up less during movement. The static scene does not get an additional pass. Changes apply after restarting the game.",
{"Vectores precisos para objetos animados: la ropa y las armas se deshacen menos al moverse. La escena estática no recibe una pasada adicional. Los cambios se aplican al reiniciar el juego.",
"Vettori accurati per gli oggetti animati: vestiti e armi si deformano meno durante il movimento. La scena statica non riceve un passaggio aggiuntivo. Le modifiche si applicano dopo il riavvio del gioco.",
"Vecteurs précis pour les objets animés : vêtements et armes se dégradent moins pendant le mouvement. La scène statique n'a pas de passe supplémentaire. Les changements s'appliquent après le redémarrage du jeu.",
"Genaue Vektoren für animierte Objekte: Kleidung und Waffen lösen sich bei Bewegung weniger auf. Die statische Szene bekommt keinen zusätzlichen Durchlauf. Änderungen gelten nach einem Neustart des Spiels.",
"Vetores precisos para objetos animados: roupas e armas se desfazem menos em movimento. A cena estática não recebe uma passada adicional. As mudanças valem após reiniciar o jogo.",
"为动画物体提供精确的矢量:衣物和武器在移动时更少破碎。静态场景不会增加额外的通道。重启游戏后生效。"}},
{"Show motion vectors (debug)",
{"Mostrar vectores de movimiento (depuración)",
"Mostra vettori di movimento (debug)",
"Afficher les vecteurs de mouvement (debug)",
"Bewegungsvektoren anzeigen (Debug)",
"Mostrar vetores de movimento (depuração)",
"显示运动矢量(调试)"}},
{"Red/green: horizontal/vertical motion (8 pixels = full brightness). Blue: the pixel received an accurate object vector, not just camera motion. The upscaler treats a moving object with no blue or red/green as stationary, causing trails.",
{"Rojo/verde: movimiento horizontal/vertical (8 píxeles = brillo máximo). Azul: el píxel ha recibido un vector de objeto preciso, no solo el movimiento de la cámara. El escalador trata un objeto en movimiento sin azul ni rojo/verde como estático, lo que causa estelas.",
"Rosso/verde: movimento orizzontale/verticale (8 pixel = luminosità piena). Blu: il pixel ha ricevuto un vettore accurato dell'oggetto, non solo il movimento della camera. L'upscaler tratta un oggetto in movimento senza blu né rosso/verde come fermo, causando scie.",
"Rouge/vert : mouvement horizontal/vertical (8 pixels = luminosité maximale). Bleu : le pixel a reçu un vecteur d'objet précis, et pas seulement le mouvement de la caméra. L'upscaler traite un objet en mouvement sans bleu ni rouge/vert comme immobile, ce qui cause des traînées.",
"Rot/Grün: horizontale/vertikale Bewegung (8 Pixel = volle Helligkeit). Blau: Der Pixel hat einen genauen Objektvektor erhalten, nicht nur die Kamerabewegung. Der Upscaler behandelt ein bewegtes Objekt ohne Blau oder Rot/Grün als ruhend, was Schlieren verursacht.",
"Vermelho/verde: movimento horizontal/vertical (8 pixels = brilho máximo). Azul: o pixel recebeu um vetor de objeto preciso, não só o movimento da câmera. O upscaler trata um objeto em movimento sem azul nem vermelho/verde como parado, o que causa rastros.",
"红/绿:水平/垂直运动(8 像素 = 最大亮度)。蓝:该像素获得了精确的物体矢量,而不只是镜头运动。超分辨率会把既没有蓝色也没有红/绿的运动物体当作静止,从而产生拖尾。"}},
{"Output resolution",
{"Resolución de salida",
"Risoluzione di uscita",
"Résolution de sortie",
"Ausgabeauflösung",
"Resolução de saída",
"输出分辨率"}},
{"Size of the final frame and UI. The preset sets the scene size relative to the output: 4K Performance = 1920x1080. Applies after restarting the game.",
{"Tamaño del fotograma final y de la interfaz. El preajuste fija el tamaño de la escena respecto a la salida: 4K Performance = 1920x1080. Se aplica al reiniciar el juego.",
"Dimensione del fotogramma finale e dell'interfaccia. Il preset imposta la dimensione della scena rispetto all'uscita: 4K Performance = 1920x1080. Si applica dopo il riavvio del gioco.",
"Taille de l'image finale et de l'interface. Le préréglage définit la taille de la scène par rapport à la sortie : 4K Performance = 1920x1080. S'applique après le redémarrage du jeu.",
"Größe des finalen Frames und der UI. Die Voreinstellung legt die Szenengröße relativ zur Ausgabe fest: 4K Performance = 1920x1080. Gilt nach einem Neustart des Spiels.",
"Tamanho do quadro final e da interface. A predefinição define o tamanho da cena em relação à saída: 4K Performance = 1920x1080. Vale após reiniciar o jogo.",
"最终画面和界面的尺寸。预设会相对输出设定场景尺寸:4K Performance = 1920x1080。重启游戏后生效。"}},
{"The final frame and UI size changes at the next frame boundary. The preset sets the scene size relative to the output: 4K Performance = 1920x1080. Changing the size resets the upscaler's history and may cause a brief pause.",
{"El tamaño del fotograma final y de la interfaz cambia en el siguiente límite de fotograma. El preajuste fija el tamaño de la escena respecto a la salida: 4K Performance = 1920x1080. Al cambiar el tamaño se reinicia el historial del escalador y puede haber una breve pausa.",
"La dimensione del fotogramma finale e dell'interfaccia cambia al fotogramma successivo. Il preset imposta la dimensione della scena rispetto all'uscita: 4K Performance = 1920x1080. Cambiare la dimensione azzera la cronologia dell'upscaler e può causare una breve pausa.",
"La taille de l'image finale et de l'interface change à la prochaine image. Le préréglage définit la taille de la scène par rapport à la sortie : 4K Performance = 1920x1080. Changer la taille réinitialise l'historique de l'upscaler et peut causer une courte pause.",
"Größe des finalen Frames und der UI ändert sich an der nächsten Frame-Grenze. Die Voreinstellung legt die Szenengröße relativ zur Ausgabe fest: 4K Performance = 1920x1080. Eine Größenänderung setzt den Verlauf des Upscalers zurück und kann eine kurze Pause verursachen.",
"O tamanho do quadro final e da interface muda no próximo limite de quadro. A predefinição define o tamanho da cena em relação à saída: 4K Performance = 1920x1080. Mudar o tamanho reinicia o histórico do upscaler e pode causar uma breve pausa.",
"最终画面和界面的尺寸会在下一帧更改。预设会相对输出设定场景尺寸:4K Performance = 1920x1080。更改尺寸会重置超分辨率的历史帧,可能会短暂停顿。"}},
{"Auto (based on GPU)",
{"Automático (según la GPU)",
"Auto (in base alla GPU)",
"Auto (selon le GPU)",
"Auto (je nach GPU)",
"Automático (pela GPU)",
"自动(按显卡)"}},
{"Off (faster)",
{"Desactivado (más rápido)",
"Disattivato (più veloce)",
"Désactivé (plus rapide)",
"Aus (schneller)",
"Desativado (mais rápido)",
"关闭(更快)"}},
{"On",
{"Activado",
"Attivato",
"Activé",
"An",
"Ativado",
"开启"}},
{"Live resolution changes",
{"Cambio de resolución en tiempo real",
"Cambio di risoluzione in tempo reale",
"Changement de résolution en direct",
"Live-Auflösungswechsel",
"Mudança de resolução em tempo real",
"实时更改分辨率"}},
{"On: output resolution and preset change without restarting, but game post-processing stays at 1080p, which is noticeably slower on Steam Deck and older GPUs. Off: everything renders at the preset resolution, and changes require a restart. Auto enables this on powerful discrete GPUs. Applies after restarting the game.",
{"Activado: la resolución de salida y el preajuste cambian sin reiniciar, pero el posprocesado del juego se queda en 1080p, bastante más lento en la Steam Deck y en GPU antiguas. Desactivado: todo se renderiza en la resolución del preajuste y los cambios requieren reiniciar. Automático lo activa en GPU dedicadas potentes. Se aplica al reiniciar el juego.",
"Attivato: risoluzione di uscita e preset cambiano senza riavviare, ma la post-elaborazione del gioco resta a 1080p, il che è sensibilmente più lento su Steam Deck e GPU meno recenti. Disattivato: tutto viene renderizzato alla risoluzione del preset e i cambi richiedono un riavvio. Auto lo attiva sulle GPU discrete potenti. Si applica dopo il riavvio del gioco.",
"Activé : la résolution de sortie et le préréglage changent sans redémarrage, mais le post-traitement du jeu reste en 1080p, ce qui est nettement plus lent sur Steam Deck et les anciens GPU. Désactivé : tout est rendu à la résolution du préréglage, et les changements nécessitent un redémarrage. Auto l'active sur les GPU dédiés puissants. S'applique après le redémarrage du jeu.",
"An: Ausgabeauflösung und Voreinstellung ändern sich ohne Neustart, aber die Nachbearbeitung des Spiels bleibt bei 1080p, was auf dem Steam Deck und älteren GPUs deutlich langsamer ist. Aus: Alles rendert in der Auflösung der Voreinstellung, Änderungen erfordern einen Neustart. Auto aktiviert dies auf leistungsstarken dedizierten GPUs. Gilt nach einem Neustart des Spiels.",
"Ativado: a resolução de saída e a predefinição mudam sem reiniciar, mas o pós-processamento do jogo fica em 1080p, bem mais lento no Steam Deck e em GPUs antigas. Desativado: tudo é renderizado na resolução da predefinição e as mudanças exigem reiniciar. Automático ativa em GPUs dedicadas potentes. Vale após reiniciar o jogo.",
"开启:无需重启即可更改输出分辨率和预设,但游戏的后期处理保持 1080p,在 Steam Deck 和较旧的显卡上明显更慢。关闭:一切都按预设分辨率渲染,更改需要重启。“自动”会在性能强的独立显卡上开启。重启游戏后生效。"}},
{"Game effects (restart required)",
{"Efectos del juego (requiere reinicio)",
"Effetti del gioco (riavvio necessario)",
"Effets du jeu (redémarrage requis)",
"Spieleffekte (Neustart nötig)",
"Efeitos do jogo (requer reinício)",
"游戏效果(需要重启)"}},
{"Highest (-2)",
{"Máximo (-2)",
"Massimo (-2)",
"Maximal (-2)",
"Höchste (-2)",
"Máximo (-2)",
"最高(-2)"}},
{"Game default",
{"Predeterminado del juego",
"Predefinito del gioco",
"Comme dans le jeu",
"Spielstandard",
"Padrão do jogo",
"游戏默认"}},
{"Lower (1)",
{"Bajo (1)",
"Basso (1)",
"Plus bas (1)",
"Niedriger (1)",
"Baixo (1)",
"较低(1)"}},
{"Lowest (2)",
{"Mínimo (2)",
"Minimo (2)",
"Minimal (2)",
"Niedrigste (2)",
"Mínimo (2)",
"最低(2)"}},
{"Model detail",
{"Detalle de modelos",
"Dettaglio dei modelli",
"Détail des modèles",
"Modelldetails",
"Detalhe dos modelos",
"模型细节"}},
{"Effects are enabled and disabled by game patches at startup (patches/Bloodborne.xml). Motion blur and shadows from dynamic lights place a significant load on the GPU.",
{"Los parches del juego activan y desactivan los efectos al iniciar (patches/Bloodborne.xml). El desenfoque de movimiento y las sombras de luces dinámicas suponen una carga considerable para la GPU.",
"Gli effetti vengono attivati e disattivati dalle patch del gioco all'avvio (patches/Bloodborne.xml). Motion blur e ombre delle luci dinamiche pesano molto sulla GPU.",
"Les effets sont activés et désactivés par des correctifs du jeu au démarrage (patches/Bloodborne.xml). Le flou de mouvement et les ombres des lumières dynamiques pèsent lourd sur le GPU.",
"Effekte werden beim Start durch Spiel-Patches aktiviert und deaktiviert (patches/Bloodborne.xml). Bewegungsunschärfe und Schatten dynamischer Lichter belasten die GPU erheblich.",
"Os efeitos são ativados e desativados por patches do jogo no início (patches/Bloodborne.xml). O desfoque de movimento e as sombras de luzes dinâmicas pesam bastante na GPU.",
"效果由启动时的游戏补丁开启或关闭(patches/Bloodborne.xml)。动态模糊和动态光源的阴影会给显卡带来明显负担。"}},
{"Free camera: hold Cross and press L3 (keyboard: Space + Z). Debug menu: left touchpad / Tab. Requires DbgFont14h.ccm and DbgFont14h.tpf in dvdroot_ps4/font from Nexus mod #253. Right touchpad: Backspace.",
{"Cámara libre: mantén Cross y pulsa L3 (teclado: Space + Z). Debug menu: panel táctil izquierdo / Tab. Requiere DbgFont14h.ccm y DbgFont14h.tpf en dvdroot_ps4/font del mod de Nexus n.º 253. Panel táctil derecho: Backspace.",
"Camera libera: tieni premuto Cross e premi L3 (tastiera: Space + Z). Debug menu: touchpad sinistro / Tab. Richiede DbgFont14h.ccm e DbgFont14h.tpf in dvdroot_ps4/font dalla mod Nexus #253. Touchpad destro: Backspace.",
"Caméra libre : maintenez Cross et appuyez sur L3 (clavier : Space + Z). Debug menu : pavé tactile gauche / Tab. Nécessite DbgFont14h.ccm et DbgFont14h.tpf dans dvdroot_ps4/font, depuis le mod Nexus #253. Pavé tactile droit : Backspace.",
"Freie Kamera: Cross halten und L3 drücken (Tastatur: Space + Z). Debug menu: linkes Touchpad / Tab. Benötigt DbgFont14h.ccm und DbgFont14h.tpf in dvdroot_ps4/font aus Nexus-Mod #253. Rechtes Touchpad: Backspace.",
"Câmera livre: segure Cross e aperte L3 (teclado: Space + Z). Debug menu: touchpad esquerdo / Tab. Requer DbgFont14h.ccm e DbgFont14h.tpf em dvdroot_ps4/font, do mod n.º 253 do Nexus. Touchpad direito: Backspace.",
"自由视角:按住 Cross 再按 L3(键盘:Space + Z)。调试菜单:触摸板左侧 / Tab。需要 Nexus 模组 #253 中的 DbgFont14h.ccm 和 DbgFont14h.tpf,放在 dvdroot_ps4/font 中。触摸板右侧:Backspace。"}},
{"Changes apply after restarting the game",
{"Los cambios se aplican al reiniciar el juego",
"Le modifiche si applicano dopo il riavvio del gioco",
"Les changements s'appliquent après le redémarrage du jeu",
"Änderungen gelten nach einem Neustart des Spiels",
"As mudanças valem após reiniciar o jogo",
"重启游戏后生效"}},
{"Apply and restart game",
{"Aplicar y reiniciar el juego",
"Applica e riavvia il gioco",
"Appliquer et redémarrer le jeu",
"Anwenden und Spiel neu starten",
"Aplicar e reiniciar o jogo",
"应用并重启游戏"}},
{"Other",
{"Otros",
"Altro",
"Autre",
"Sonstiges",
"Outros",
"其他"}},
{"FPS counter in corner",
{"Contador de FPS en la esquina",
"Contatore FPS nell'angolo",
"Compteur de FPS dans le coin",
"FPS-Anzeige in der Ecke",
"Contador de FPS no canto",
"角落显示 FPS 计数器"}},
{"Close",
{"Cerrar",
"Chiudi",
"Fermer",
"Schließen",
"Fechar",
"关闭"}},
{"Settings are saved to bbport.ini",
{"Los ajustes se guardan en bbport.ini",
"Le impostazioni vengono salvate in bbport.ini",
"Les paramètres sont enregistrés dans bbport.ini",
"Einstellungen werden in bbport.ini gespeichert",
"As configurações são salvas em bbport.ini",
"设置保存在 bbport.ini 中"}},
{"%.0f FPS %.1f ms %s",
{"%.0f FPS %.1f ms %s",
"%.0f FPS %.1f ms %s",
"%.0f FPS %.1f ms %s",
"%.0f FPS %.1f ms %s",
"%.0f FPS %.1f ms %s",
"%.0f FPS %.1f 毫秒 %s"}},
{"Chromatic aberration",
{"Aberración cromática",
"Aberrazione cromatica",
"Aberration chromatique",
"Chromatische Aberration",
"Aberração cromática",
"色差"}},
{"Depth of field (DoF)",
{"Profundidad de campo (DoF)",
"Profondità di campo (DoF)",
"Profondeur de champ (DoF)",
"Tiefenunschärfe (DoF)",
"Profundidade de campo (DoF)",
"景深(DoF)"}},
{"Motion blur",
{"Desenfoque de movimiento",
"Motion blur",
"Flou de mouvement",
"Bewegungsunschärfe",
"Desfoque de movimento",
"动态模糊"}},
{"Ambient occlusion (SSAO)",
{"Oclusión ambiental (SSAO)",
"Occlusione ambientale (SSAO)",
"Occlusion ambiante (SSAO)",
"Ambient Occlusion (SSAO)",
"Oclusão ambiente (SSAO)",
"环境光遮蔽(SSAO)"}},
{"Game's own anti-aliasing",
{"Antialiasing propio del juego",
"Anti-aliasing del gioco",
"Anticrénelage propre au jeu",
"Spieleigene Kantenglättung",
"Anti-aliasing do próprio jogo",
"游戏自带的抗锯齿"}},
{"Shadows from dynamic lights",
{"Sombras de luces dinámicas",
"Ombre delle luci dinamiche",
"Ombres des lumières dynamiques",
"Schatten dynamischer Lichter",
"Sombras de luzes dinâmicas",
"动态光源的阴影"}},
{"Screen-space reflections (not in original game)",
{"Reflejos en espacio de pantalla (no están en el juego original)",
"Riflessi nello spazio dello schermo (SSR) (assenti nel gioco originale)",
"Réflexions SSR (absentes du jeu original)",
"Screen-Space-Reflexionen (nicht im Originalspiel)",
"Reflexos em espaço de tela (não existem no jogo original)",
"屏幕空间反射(原版游戏没有)"}},
{"Skip startup intros",
{"Saltar las intros de inicio",
"Salta le intro all'avvio",
"Passer les vidéos d'introduction",
"Start-Intros überspringen",
"Pular as intros de início",
"跳过启动动画"}},
{"Free camera (Cross + L3)",
{"Cámara libre (Cross + L3)",
"Camera libera (Cross + L3)",
"Caméra libre (Cross + L3)",
"Freie Kamera (Cross + L3)",
"Câmera livre (Cross + L3)",
"自由视角(Cross + L3)"}},
{"Debug menu (requires font files)",
{"Debug menu (requiere archivos de fuente)",
"Debug menu (richiede i file dei font)",
"Debug menu (fichiers de police requis)",
"Debug menu (benötigt Schriftdateien)",
"Debug menu (requer arquivos de fonte)",
"调试菜单(需要字体文件)"}},
{"Cheat: never die (not below 1 HP)",
{"Truco: no morir (nunca por debajo de 1 HP)",
"Cheat: non morire mai (mai sotto 1 HP)",
"Triche : ne jamais mourir (pas sous 1 HP)",
"Cheat: nie sterben (nicht unter 1 HP)",
"Truque: não morrer (nunca abaixo de 1 HP)",
"作弊:永不死亡(不低于 1 HP)"}},
{"Cheat: enemies do not see you",
{"Truco: los enemigos no te ven",
"Cheat: i nemici non ti vedono",
"Triche : les ennemis ne vous voient pas",
"Cheat: Gegner sehen dich nicht",
"Truque: os inimigos não te veem",
"作弊:敌人看不见你"}},
{"Cheat: enemies do not hear you",
{"Truco: los enemigos no te oyen",
"Cheat: i nemici non ti sentono",
"Triche : les ennemis ne vous entendent pas",
"Cheat: Gegner hören dich nicht",
"Truque: os inimigos não te ouvem",
"作弊:敌人听不见你"}},
{"Cheat: Rally never fades",
{"Truco: Rally nunca se desvanece",
"Cheat: Rally non svanisce mai",
"Triche : le Rally ne s'estompe jamais",
"Cheat: Rally verblasst nie",
"Truque: o Rally nunca some",
"作弊:Rally 永不消退"}},
{"Cheat: enemy control (R3 / L3)",
{"Truco: control de enemigos (R3 / L3)",
"Cheat: controllo dei nemici (R3 / L3)",
"Triche : contrôle des ennemis (R3 / L3)",
"Cheat: Gegnerkontrolle (R3 / L3)",
"Truque: controle de inimigos (R3 / L3)",
"作弊:控制敌人(R3 / L3)"}},
{"No Rally (health regain)",
{"Sin Rally (recuperación de salud)",
"Niente Rally (recupero di salute)",
"Pas de Rally (regain de santé)",
"Ohne Rally (Lebensrückgewinn)",
"Sem Rally (recuperação de vida)",
"无 Rally(回血)"}},
{"Camera further away",
{"Cámara más alejada",
"Camera più lontana",
"Caméra plus éloignée",
"Kamera weiter entfernt",
"Câmera mais afastada",
"镜头更远"}},
{"No camera auto-rotation",
{"Sin giro automático de la cámara",
"Niente rotazione automatica della camera",
"Pas de rotation automatique de la caméra",
"Keine automatische Kameradrehung",
"Sem giro automático da câmera",
"关闭镜头自动旋转"}},
{"Run with less stick tilt",
{"Correr con menos inclinación del stick",
"Corri con meno inclinazione dello stick",
"Courir en inclinant moins le stick",
"Laufen bei geringerer Stick-Neigung",
"Correr com menos inclinação do analógico",
"摇杆推得更少也能奔跑"}},
{"Dark Souls ragdoll physics",
{"Físicas ragdoll de Dark Souls",
"Fisica ragdoll di Dark Souls",
"Physique ragdoll de Dark Souls",
"Ragdoll-Physik wie in Dark Souls",
"Física ragdoll de Dark Souls",
"黑暗之魂的布娃娃物理"}},
// The game's System menu pages (bbport_game_menu.cpp).
{"Display",
{"Imagen",
"Immagine",
"Image",
"Bild",
"Imagem",
"画面"}},
{"Resolution, upscaler, sharpness and model detail (bbport)",
{"Resolución, escalador, nitidez y detalle de modelos (bbport)",
"Risoluzione, upscaler, nitidezza e dettaglio dei modelli (bbport)",
"Résolution, upscaler, netteté et détail des modèles (bbport)",
"Auflösung, Upscaler, Schärfe und Modelldetails (bbport)",
"Resolução, upscaler, nitidez e detalhe dos modelos (bbport)",
"分辨率、超分辨率、锐度和模型细节(bbport)"}},
{"Quality preset",
{"Preajuste de calidad",
"Preset di qualità",
"Préréglage de qualité",
"Qualitätsstufe",
"Predefinição de qualidade",
"画质预设"}},
{"Scene resolution: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
{"Resolución de la escena: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
"Risoluzione della scena: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
"Résolution de la scène : 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
"Szenenauflösung: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
"Resolução da cena: 0-1 Native AA, 2-3 Quality, 4-6 Balanced, 7-8 Performance, 9-10 Ultra",
"场景分辨率:0-1 Native AA,2-3 Quality,4-6 Balanced,7-8 Performance,9-10 Ultra"}},
{"RCAS after the upscaler: 0 off, 10 strongest",
{"RCAS tras el escalador: 0 desactivado, 10 el más fuerte",
"RCAS dopo l'upscaler: 0 disattivato, 10 il più forte",
"RCAS après l'upscaler : 0 désactivé, 10 le plus fort",
"RCAS nach dem Upscaler: 0 aus, 10 am stärksten",
"RCAS após o upscaler: 0 desativado, 10 o mais forte",
"超分辨率之后的 RCAS:0 关闭,10 最强"}},
{"Final frame: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
{"Imagen final: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
"Immagine finale: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
"Image finale : 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
"Fertiges Bild: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
"Imagem final: 0-1 1280x720, 2-4 1920x1080, 5-7 2560x1440, 8-10 3840x2160",
"最终画面:0-1 1280x720,2-4 1920x1080,5-7 2560x1440,8-10 3840x2160"}},
{"FPS counter",
{"Contador de FPS",
"Contatore FPS",
"Compteur de FPS",
"FPS-Anzeige",
"Contador de FPS",
"FPS 计数器"}},
{"Frame rate in the top right corner",
{"Fotogramas por segundo en la esquina superior derecha",
"Fotogrammi al secondo nell'angolo in alto a destra",
"Images par seconde dans le coin supérieur droit",
"Bildrate in der oberen rechten Ecke",
"Quadros por segundo no canto superior direito",
"右上角显示帧率"}},
{"After a restart: 0-1 highest, 2-4 game default, 5-7 lower, 8-10 lowest",
{"Tras reiniciar: 0-1 máximo, 2-4 como en el juego, 5-7 menor, 8-10 mínimo",
"Dopo un riavvio: 0-1 massimo, 2-4 come nel gioco, 5-7 inferiore, 8-10 minimo",
"Après un redémarrage : 0-1 maximal, 2-4 comme dans le jeu, 5-7 réduit, 8-10 minimal",
"Nach einem Neustart: 0-1 höchste, 2-4 Spielstandard, 5-7 niedriger, 8-10 niedrigste",
"Após reiniciar: 0-1 máximo, 2-4 como no jogo, 5-7 menor, 8-10 mínimo",
"重启后生效:0-1 最高,2-4 游戏默认,5-7 较低,8-10 最低"}},
{"Highest",
{"Máximo",
"Massimo",
"Maximal",
"Höchste",
"Máximo",
"最高"}},
{"Lower",
{"Menor",
"Inferiore",
"Réduit",
"Niedriger",
"Menor",
"较低"}},
{"Lowest",
{"Mínimo",
"Minimo",
"Minimal",
"Niedrigste",
"Mínimo",
"最低"}},
{"Temporal upscaler and anti-aliasing",
{"Escalador temporal y suavizado",
"Upscaler temporale e anti-aliasing",
"Upscaler temporel et anticrénelage",
"Temporaler Upscaler und Kantenglättung",
"Upscaler temporal e anti-aliasing",
"时间性超分辨率和抗锯齿"}},
{"Effects",
{"Efectos",
"Effetti",
"Effets",
"Effekte",
"Efeitos",
"效果"}},
{"The game's effects (bbport; after a restart)",
{"Los efectos del juego (bbport; tras reiniciar)",
"Gli effetti del gioco (bbport; dopo un riavvio)",
"Les effets du jeu (bbport ; après un redémarrage)",
"Die Effekte des Spiels (bbport; nach einem Neustart)",
"Os efeitos do jogo (bbport; após reiniciar)",
"游戏的效果(bbport;重启后生效)"}},
{"Colour fringes at the frame edges (after a restart)",
{"Bordes de color en los extremos de la imagen (tras reiniciar)",
"Bordi colorati ai margini dell'immagine (dopo un riavvio)",
"Franges colorées sur les bords de l'image (après un redémarrage)",
"Farbsäume an den Bildrändern (nach einem Neustart)",
"Franjas coloridas nas bordas da imagem (após reiniciar)",
"画面边缘的色边(重启后生效)"}},
{"Depth of field",
{"Profundidad de campo",
"Profondità di campo",
"Profondeur de champ",
"Tiefenschärfe",
"Profundidade de campo",
"景深"}},
{"Blur of distant and near objects (DoF) (after a restart)",
{"Desenfoque de objetos lejanos y cercanos (DoF) (tras reiniciar)",
"Sfocatura degli oggetti lontani e vicini (DoF) (dopo un riavvio)",
"Flou des objets lointains et proches (DoF) (après un redémarrage)",
"Unschärfe ferner und naher Objekte (DoF) (nach einem Neustart)",
"Desfoque de objetos distantes e próximos (DoF) (após reiniciar)",
"远处和近处物体的模糊(DoF)(重启后生效)"}},
{"Blur when the camera turns (after a restart)",
{"Desenfoque al girar la cámara (tras reiniciar)",
"Sfocatura quando la telecamera ruota (dopo un riavvio)",
"Flou quand la caméra tourne (après un redémarrage)",
"Unschärfe beim Drehen der Kamera (nach einem Neustart)",
"Desfoque ao girar a câmera (após reiniciar)",
"镜头转动时的模糊(重启后生效)"}},
{"Ambient occlusion",
{"Oclusión ambiental",
"Occlusione ambientale",
"Occlusion ambiante",
"Umgebungsverdeckung",
"Oclusão de ambiente",
"环境光遮蔽"}},
{"Shading in corners and contacts (SSAO) (after a restart)",
{"Sombreado en esquinas y zonas de contacto (SSAO) (tras reiniciar)",
"Ombreggiatura negli angoli e nei punti di contatto (SSAO) (dopo un riavvio)",
"Ombrage dans les coins et aux contacts (SSAO) (après un redémarrage)",
"Schattierung in Ecken und an Kontaktstellen (SSAO) (nach einem Neustart)",
"Sombreamento em cantos e pontos de contato (SSAO) (após reiniciar)",
"角落和接触处的阴影(SSAO)(重启后生效)"}},
{"Dynamic shadows",
{"Sombras dinámicas",
"Ombre dinamiche",
"Ombres dynamiques",
"Dynamische Schatten",
"Sombras dinâmicas",
"动态阴影"}},
{"Shadows from dynamic lights (after a restart)",
{"Sombras de las luces dinámicas (tras reiniciar)",
"Ombre delle luci dinamiche (dopo un riavvio)",
"Ombres des lumières dynamiques (après un redémarrage)",
"Schatten dynamischer Lichtquellen (nach einem Neustart)",
"Sombras das luzes dinâmicas (após reiniciar)",
"动态光源的阴影(重启后生效)"}},
{"SSR reflections",
{"Reflejos SSR",
"Riflessi SSR",
"Reflets SSR",
"SSR-Spiegelungen",
"Reflexos SSR",
"SSR 反射"}},
{"Screen-space reflections (not in the original) (after a restart)",
{"Reflejos en espacio de pantalla (no estaban en el original) (tras reiniciar)",
"Riflessi in spazio schermo (non presenti nell'originale) (dopo un riavvio)",
"Reflets en espace écran (absents de l'original) (après un redémarrage)",
"Bildschirmraum-Spiegelungen (nicht im Original) (nach einem Neustart)",
"Reflexos em espaço de tela (não existiam no original) (após reiniciar)",
"屏幕空间反射(原版没有)(重启后生效)"}},
{"NVIDIA DLSS Super Resolution (RTX GPUs; Native AA is DLAA). Needs the DLSS bridge and NVIDIA's library next to the game; otherwise FSR 3.1 is used.",
{"NVIDIA DLSS Super Resolution (GPU RTX; Native AA es DLAA). Necesita el puente de DLSS y la biblioteca de NVIDIA junto al juego; si no, se usa FSR 3.1.",
"NVIDIA DLSS Super Resolution (GPU RTX; Native AA è DLAA). Richiede il bridge DLSS e la libreria di NVIDIA accanto al gioco; altrimenti si usa FSR 3.1.",
"NVIDIA DLSS Super Resolution (GPU RTX ; Native AA correspond au DLAA). Nécessite le pont DLSS et la bibliothèque de NVIDIA à côté du jeu ; sinon, FSR 3.1 est utilisé.",
"NVIDIA DLSS Super Resolution (RTX-GPUs; Native AA ist DLAA). Benötigt die DLSS-Brücke und NVIDIAs Bibliothek neben dem Spiel; sonst wird FSR 3.1 verwendet.",
"NVIDIA DLSS Super Resolution (GPUs RTX; Native AA é DLAA). Precisa da ponte do DLSS e da biblioteca da NVIDIA junto ao jogo; caso contrário, usa-se o FSR 3.1.",
"NVIDIA DLSS Super Resolution(RTX 显卡;Native AA 即 DLAA)。需要 DLSS 桥接和放在游戏旁的 NVIDIA 库;否则使用 FSR 3.1。"}},
{"Hold Cross and press L3 (keyboard: Space + Z).",
{"Mantén Cruz y pulsa L3 (teclado: Espacio + Z).",
"Tieni premuto Croce e premi L3 (tastiera: Spazio + Z).",
"Maintenez Croix et appuyez sur L3 (clavier : Espace + Z).",
"Halte Kreuz gedrückt und drücke L3 (Tastatur: Leertaste + Z).",
"Segure X e aperte L3 (teclado: Espaço + Z).",
"按住 Cross 再按 L3(键盘:Space + Z)。"}},
{"Left side of the touchpad (Tab); right side: Backspace. Needs the adhoc folder from Nexus mod #253 (the fonts in adhoc/font).",
{"Lado izquierdo del panel táctil (Tab); lado derecho: Retroceso. Necesita la carpeta adhoc del mod #253 de Nexus (las fuentes en adhoc/font).",
"Lato sinistro del touchpad (Tab); lato destro: Backspace. Richiede la cartella adhoc della mod #253 di Nexus (i font in adhoc/font).",
"Côté gauche du pavé tactile (Tab) ; côté droit : Retour arrière. Nécessite le dossier adhoc du mod Nexus n° 253 (les polices dans adhoc/font).",
"Linke Seite des Touchpads (Tab); rechte Seite: Rücktaste. Benötigt den Ordner adhoc aus Nexus-Mod #253 (die Schriften in adhoc/font).",
"Lado esquerdo do touchpad (Tab); lado direito: Backspace. Precisa da pasta adhoc do mod #253 do Nexus (as fontes em adhoc/font).",
"触摸板左侧(Tab);右侧:Backspace。需要 Nexus 模组 #253 的 adhoc 文件夹(字体位于 adhoc/font)。"}},
{"Quit game",
{"Salir del juego",
"Esci dal gioco",
"Quitter le jeu",
"Spiel beenden",
"Sair do jogo",
"退出游戏"}},
{"Press again to quit",
{"Pulsa otra vez para salir",
"Premi di nuovo per uscire",
"Appuyez à nouveau pour quitter",
"Zum Beenden erneut drücken",
"Aperte de novo para sair",
"再按一次退出"}},
{"Caps",
{"Mayús",
"Maiusc",
"Maj",
"Umschalt",
"Maiúsc",
"大写"}},
{"Space",
{"Espacio",
"Spazio",
"Espace",
"Leerzeichen",
"Espaço",
"空格"}},
{"Delete",
{"Borrar",
"Cancella",
"Effacer",
"Löschen",
"Apagar",
"删除"}},
{"Clear",
{"Vaciar",
"Svuota",
"Vider",
"Leeren",
"Limpar",
"清空"}},
{"Cancel",
{"Cancelar",
"Annulla",
"Annuler",
"Abbrechen",
"Cancelar",
"取消"}},
{"Controller: D-pad or left stick moves, Cross types, Square deletes, Triangle is a space, L1/R1 caps, Options = OK, Circle = cancel",
{"Mando: cruceta o stick izquierdo para moverse, Cruz escribe, Cuadrado borra, Triángulo es un espacio, L1/R1 mayúsculas, Options = OK, Círculo = cancelar",
"Controller: croce direzionale o levetta sinistra per muoversi, Croce scrive, Quadrato cancella, Triangolo è uno spazio, L1/R1 maiuscole, Options = OK, Cerchio = annulla",
"Manette : croix directionnelle ou stick gauche pour se déplacer, Croix tape, Carré efface, Triangle fait un espace, L1/R1 majuscules, Options = OK, Rond = annuler",
"Controller: Steuerkreuz oder linker Stick bewegt, Kreuz tippt, Quadrat löscht, Dreieck ist ein Leerzeichen, L1/R1 Großbuchstaben, Options = OK, Kreis = abbrechen",
"Controle: direcional ou analógico esquerdo move, X digita, Quadrado apaga, Triângulo é um espaço, L1/R1 maiúsculas, Options = OK, Círculo = cancelar",
"手柄:方向键或左摇杆移动,Cross 输入,Square 删除,Triangle 空格,L1/R1 切换大小写,Options = 确定,Circle = 取消"}},
{"Keyboard: type, Backspace = delete, Enter = OK, Esc = cancel",
{"Teclado: escribe, Retroceso = borrar, Intro = OK, Esc = cancelar",
"Tastiera: scrivi, Backspace = cancella, Invio = OK, Esc = annulla",
"Clavier : tapez, Retour arrière = effacer, Entrée = OK, Échap = annuler",
"Tastatur: tippen, Rücktaste = löschen, Enter = OK, Esc = abbrechen",
"Teclado: digite, Backspace = apagar, Enter = OK, Esc = cancelar",
"键盘:直接输入,Backspace = 删除,Enter = 确定,Esc = 取消"}},
{"Notice while shaders compile",
{"Aviso mientras se compilan shaders",
"Avviso durante la compilazione degli shader",
"Avis pendant la compilation des shaders",
"Hinweis beim Kompilieren von Shadern",
"Aviso enquanto os shaders compilam",
"编译着色器时显示提示"}},
{"\"Compiling shaders\" in the bottom-left corner: a stutter then is the shader cache filling, not a problem. Each place only once.",
{"«Compilando shaders» en la esquina inferior izquierda: un tirón en ese momento es la caché de shaders llenándose, no un problema. Solo una vez en cada sitio.",
"«Compilazione shader» nell'angolo in basso a sinistra: uno scatto in quel momento è la cache degli shader che si riempie, non un problema. Solo una volta per ogni luogo.",
"« Compilation des shaders » dans le coin inférieur gauche : une saccade à ce moment-là, c'est le cache de shaders qui se remplit, pas un problème. Une seule fois par endroit.",
"„Shader werden kompiliert“ unten links: Ein Ruckler in diesem Moment ist der Shader-Cache, der sich füllt, kein Problem. An jedem Ort nur einmal.",
"\"Compilando shaders\" no canto inferior esquerdo: um travamento nesse momento é o cache de shaders se enchendo, não um problema. Só uma vez em cada lugar.",
"左下角显示“正在编译着色器”:此时的卡顿是着色器缓存在填充,并非问题。每个地方只会出现一次。"}},
{"Compiling shaders…",
{"Compilando shaders…",
"Compilazione shader…",
"Compilation des shaders…",
"Shader werden kompiliert…",
"Compilando shaders…",
"正在编译着色器…"}},
};
const int column = int(language) - First;
if (column < 0 || column >= int(Row{}.size())) {
return english;
}
const auto it = table.find(english);
return it != table.end() ? it->second[column] : english;
}
} // namespace BbSettings
+428 -11
View File
@@ -4,18 +4,25 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cfloat>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <mutex>
#include <string>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>
#include <SDL3/SDL.h>
#include "bbport_settings.h"
#include "bbport_shader_activity.h"
#include "imgui.h"
#include "imgui_impl_vulkan.h"
#include "imgui_internal.h" // RegisterUserTexture: the trophies' icons
#include "video_core/renderer_vulkan/vk_instance.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
@@ -50,11 +57,67 @@ float base_scale = 1.0f;
std::mutex prompt_mutex;
std::atomic<bool> prompt_active{false};
std::string prompt_title, prompt_text;
// bbport: an on-screen keyboard in the text dialog, for a controller (Steam Deck, the sofa): the
// name could only be typed on a physical keyboard. From SrShadowy/bloodborne_pc. Its state is
// kept under imgui_mutex (HandleEvent, Render); the edits go to the window thread, which owns the
// text (TakeTextEdits), so the render thread never changes the window.
struct OnScreenKeyboard {
int row = 1, col = 0;
bool caps = true;
u64 last_stick_ms = 0;
} osk;
std::vector<TextEdit> text_edits; // under prompt_mutex
constexpr const char* OskKeys[4][2][12] = {
{{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "_"},
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "_"}},
{{"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"}, {"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}},
{{"A", "S", "D", "F", "G", "H", "J", "K", "L", "'"}, {"a", "s", "d", "f", "g", "h", "j", "k", "l", "'"}},
{{"Z", "X", "C", "V", "B", "N", "M", ",", ".", "/"}, {"z", "x", "c", "v", "b", "n", "m", ",", ".", "/"}},
};
// The letters of the menu's language that the English rows lack (the game takes them in a name:
// ñ typed on a physical keyboard worked, not on this one), a row of their own above the actions.
struct OskExtraRow {
int count;
const char* keys[2][12];
};
constexpr OskExtraRow OskExtras[] = {
{0, {}}, // English
{0, {}}, // Russian
{9, {{"Ñ", "Á", "É", "Í", "Ó", "Ú", "Ü", "¿", "¡"},
{"ñ", "á", "é", "í", "ó", "ú", "ü", "¿", "¡"}}}, // Spanish
{6, {{"À", "È", "É", "Ì", "Ò", "Ù"}, {"à", "è", "é", "ì", "ò", "ù"}}}, // Italian
{12, {{"À", "Â", "Ç", "É", "È", "Ê", "Ë", "Î", "Ï", "Ô", "Ù", "Û"},
{"à", "â", "ç", "é", "è", "ê", "ë", "î", "ï", "ô", "ù", "û"}}}, // French
{4, {{"Ä", "Ö", "Ü", "ß"}, {"ä", "ö", "ü", "ß"}}}, // German
{12, {{"Á", "Â", "Ã", "À", "Ç", "É", "Ê", "Í", "Ó", "Ô", "Õ", "Ú"},
{"á", "â", "ã", "à", "ç", "é", "ê", "í", "ó", "ô", "õ", "ú"}}}, // Portuguese
{0, {}}, // Chinese
};
static_assert(std::size(OskExtras) == size_t(BbSettings::MenuLanguage::Count));
enum OskAction { OskCaps, OskSpace, OskBackspace, OskClear, OskConfirm, OskCancel, OskActions };
// Present rate for the FPS counter.
std::chrono::steady_clock::time_point last_present{};
float frame_ms_avg = 0.0f;
// Trophy notices (bbgpu_trophy_notice): one card at a time in the top-left corner, in the style
// of the PS4's, with the trophy's icon (a cup in its grade's colour when it has none).
struct Notice {
int id = 0;
char grade = '-';
std::string name;
std::vector<u8> icon; // RGBA
int icon_size = 0;
};
std::mutex notice_mutex;
std::deque<Notice> notices;
std::atomic<bool> notice_busy{false}; // queued or on screen
bool notice_shown = false; // the present thread's (imgui_mutex)
Notice notice;
std::chrono::steady_clock::time_point notice_start;
std::unordered_map<int, ImTextureData*> notice_icons; // kept: one per trophy, 64 KiB each
constexpr float NoticeIn = 0.35f, NoticeHold = 5.0f, NoticeOut = 0.6f;
float PixelDensity(SDL_WindowID id);
void SetOpen(bool value) {
@@ -139,12 +202,14 @@ void Store(std::atomic<T>& target, T value, bool changed) {
void Checkbox(const char* label, std::atomic<bool>& value) {
bool v = value;
Store(value, v, ImGui::Checkbox(label, &v));
const bool changed = ImGui::Checkbox(label, &v);
Store(value, v, changed);
}
void Slider(const char* label, std::atomic<float>& value, float lo, float hi) {
float v = value;
Store(value, v, ImGui::SliderFloat(label, &v, lo, hi, "%.2f"));
const bool changed = ImGui::SliderFloat(label, &v, lo, hi, "%.2f");
Store(value, v, changed);
}
void Hint(const char* text) {
@@ -194,11 +259,10 @@ void Menu() {
}
}
// Always readable, even when the rest of the menu is in Russian.
static const char* languages[] = {"English", "Русский"};
int language = s.menu_language == BbSettings::MenuLanguage::Russian ? 1 : 0;
if (ImGui::Combo("Language", &language, languages, 2)) {
s.menu_language =
language == 1 ? BbSettings::MenuLanguage::Russian : BbSettings::MenuLanguage::English;
int language = int(s.menu_language.load());
if (ImGui::Combo("Language", &language, BbSettings::MenuLanguageNames,
int(BbSettings::MenuLanguage::Count))) {
s.menu_language = BbSettings::MenuLanguage(language);
BbSettings::Save();
}
ImGui::Text(BbSettings::MenuText("%.0f FPS (%.1f ms)", "%.0f FPS (%.1f мс)"),
@@ -542,6 +606,13 @@ void Menu() {
ImGui::SeparatorText(BbSettings::MenuText("Other", "Прочее"));
Checkbox(BbSettings::MenuText("FPS counter in corner", "Счётчик FPS в углу"), s.show_fps);
Checkbox(BbSettings::MenuText("Notice while shaders compile", "Уведомление о компиляции шейдеров"),
s.shader_notice);
Hint(BbSettings::MenuText(
"\"Compiling shaders\" in the bottom-left corner: a stutter then is the shader cache "
"filling, not a problem. Each place only once.",
"«Компиляция шейдеров» в левом нижнем углу: рывок в это время — заполнение кэша "
"шейдеров, а не проблема. В каждом месте только один раз."));
ImGui::Spacing();
if (ImGui::Button(BbSettings::MenuText("Close", "Закрыть"))) {
@@ -594,6 +665,230 @@ void FpsCounter() {
ImGui::End();
}
// The shader notice: shown while compiling and 400 ms after, then fading out over 400 ms.
constexpr std::int64_t ShaderNoticeHoldMs = 400, ShaderNoticeFadeMs = 400;
bool ShaderNoticeDue() {
const std::int64_t idle = BbShaderActivity::IdleMs();
return BbSettings::Get().shader_notice && idle >= 0 &&
idle < ShaderNoticeHoldMs + ShaderNoticeFadeMs;
}
void ShaderNotice() {
const std::int64_t idle = BbShaderActivity::IdleMs();
const float alpha =
idle <= ShaderNoticeHoldMs ? 1.0f
: 1.0f - float(idle - ShaderNoticeHoldMs) / ShaderNoticeFadeMs;
const ImGuiViewport* viewport = ImGui::GetMainViewport();
const float pad = 12.0f * base_scale;
ImGui::SetNextWindowPos(
ImVec2(viewport->WorkPos.x + pad, viewport->WorkPos.y + viewport->WorkSize.y - pad),
ImGuiCond_Always, ImVec2(0.0f, 1.0f));
ImGui::SetNextWindowBgAlpha(0.5f * alpha);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
ImGui::Begin("##shaders", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
ImGuiWindowFlags_NoFocusOnAppearing);
ImGui::TextUnformatted(BbSettings::MenuText("Compiling shaders…", "Компиляция шейдеров…"));
ImGui::End();
ImGui::PopStyleVar();
}
ImU32 GradeColour(char grade, float alpha) {
const int a = int(255.0f * alpha);
switch (grade) {
case 'P':
return IM_COL32(170, 200, 228, a);
case 'G':
return IM_COL32(226, 186, 72, a);
case 'S':
return IM_COL32(196, 200, 206, a);
case 'B':
return IM_COL32(186, 118, 68, a);
default:
return IM_COL32(200, 200, 200, a);
}
}
// A small cup: bowl, handles, stem and base, `size` high from `top`.
void DrawCup(ImDrawList* draw, ImVec2 top, float size, ImU32 colour) {
const float w = size * 0.8f, x = top.x, y = top.y;
draw->PathLineTo(ImVec2(x, y));
draw->PathLineTo(ImVec2(x + w, y));
draw->PathBezierCubicCurveTo(ImVec2(x + w, y + size * 0.45f), ImVec2(x + w * 0.65f, y + size * 0.6f),
ImVec2(x + w * 0.5f, y + size * 0.6f));
draw->PathBezierCubicCurveTo(ImVec2(x + w * 0.35f, y + size * 0.6f), ImVec2(x, y + size * 0.45f),
ImVec2(x, y));
draw->PathFillConvex(colour);
const float handle = size * 0.12f;
draw->AddCircle(ImVec2(x - handle * 0.4f, y + size * 0.2f), handle, colour, 12, size * 0.07f);
draw->AddCircle(ImVec2(x + w + handle * 0.4f, y + size * 0.2f), handle, colour, 12, size * 0.07f);
draw->AddRectFilled(ImVec2(x + w * 0.44f, y + size * 0.58f), ImVec2(x + w * 0.56f, y + size * 0.82f),
colour);
draw->AddRectFilled(ImVec2(x + w * 0.22f, y + size * 0.82f), ImVec2(x + w * 0.78f, y + size * 0.95f),
colour, size * 0.04f);
}
ImTextureData* NoticeIcon(const Notice& n) {
if (n.icon_size <= 0) {
return nullptr;
}
auto& texture = notice_icons[n.id];
if (texture == nullptr) {
texture = new ImTextureData();
texture->Create(ImTextureFormat_RGBA32, n.icon_size, n.icon_size);
std::memcpy(texture->GetPixels(), n.icon.data(), n.icon.size());
ImGui::RegisterUserTexture(texture);
}
return texture;
}
void TrophyCard() {
const auto now = std::chrono::steady_clock::now();
if (!notice_shown) {
std::scoped_lock lock{notice_mutex};
if (notices.empty()) {
notice_busy = false;
return;
}
notice = std::move(notices.front());
notices.pop_front();
notice_shown = true;
notice_start = now;
}
const float t = std::chrono::duration<float>(now - notice_start).count();
if (t > NoticeIn + NoticeHold + NoticeOut) {
notice_shown = false;
return;
}
const float in = std::min(t / NoticeIn, 1.0f);
const float alpha = t < NoticeIn + NoticeHold ? in : 1.0f - (t - NoticeIn - NoticeHold) / NoticeOut;
const float ease = 1.0f - (1.0f - in) * (1.0f - in);
const float s = base_scale;
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImDrawList* draw = ImGui::GetForegroundDrawList();
ImFont* font = ImGui::GetFont();
const float text_size = ImGui::GetFontSize();
const float pad = 12.0f * s, icon = 72.0f * s, width = 470.0f * s;
const float text_x = pad + icon + 16.0f * s, text_w = width - text_x - pad;
const char* earned = BbSettings::MenuText("You have earned a trophy.", "Вы получили трофей.");
const ImVec2 name_size =
font->CalcTextSizeA(text_size * 1.05f, FLT_MAX, text_w, notice.name.c_str());
const float height = std::max(icon + 2.0f * pad, pad * 2.0f + text_size * 1.6f + name_size.y);
const ImVec2 p0(viewport->WorkPos.x + 36.0f * s - (1.0f - ease) * 48.0f * s,
viewport->WorkPos.y + 36.0f * s);
const ImVec2 p1(p0.x + width, p0.y + height);
draw->AddRectFilled(ImVec2(p0.x + 3.0f * s, p0.y + 4.0f * s), ImVec2(p1.x + 3.0f * s, p1.y + 4.0f * s),
IM_COL32(0, 0, 0, int(90 * alpha)), 8.0f * s);
draw->AddRectFilled(p0, p1, IM_COL32(24, 24, 30, int(232 * alpha)), 8.0f * s);
draw->AddRect(p0, p1, IM_COL32(255, 255, 255, int(28 * alpha)), 8.0f * s);
const ImVec2 icon0(p0.x + pad, p0.y + (height - icon) * 0.5f);
if (ImTextureData* texture = NoticeIcon(notice)) {
draw->AddImageRounded(texture->GetTexRef(), icon0, ImVec2(icon0.x + icon, icon0.y + icon),
ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, int(255 * alpha)),
4.0f * s);
} else {
draw->AddRectFilled(icon0, ImVec2(icon0.x + icon, icon0.y + icon),
IM_COL32(50, 50, 58, int(255 * alpha)), 4.0f * s);
DrawCup(draw, ImVec2(icon0.x + icon * 0.28f, icon0.y + icon * 0.22f), icon * 0.56f,
GradeColour(notice.grade, alpha));
}
const ImVec2 line1(p0.x + text_x, p0.y + pad + 2.0f * s);
draw->AddText(font, text_size * 0.92f, line1, IM_COL32(205, 205, 212, int(255 * alpha)), earned);
draw->AddText(font, text_size * 1.05f, ImVec2(line1.x, line1.y + text_size * 1.45f),
IM_COL32(255, 255, 255, int(255 * alpha)), notice.name.c_str(), nullptr, text_w);
}
const OskExtraRow& OskExtra() {
return OskExtras[size_t(BbSettings::Get().menu_language.load())];
}
/// The rows of characters: the four English ones, and the language's own when it has any.
int OskKeyRows() {
return OskExtra().count ? 5 : 4;
}
int OskRowLength(int row) {
return row == 0 ? 12 : row < 4 ? 10 : row < OskKeyRows() ? OskExtra().count : OskActions;
}
const char* OskKey(int row, int col) {
return row < 4 ? OskKeys[row][osk.caps ? 0 : 1][col] : OskExtra().keys[osk.caps ? 0 : 1][col];
}
const char* OskActionLabel(int action) {
switch (action) {
case OskCaps: return BbSettings::MenuText("Caps", "Регистр");
case OskSpace: return BbSettings::MenuText("Space", "Пробел");
case OskBackspace: return BbSettings::MenuText("Delete", "Стереть");
case OskClear: return BbSettings::MenuText("Clear", "Очистить");
case OskConfirm: return BbSettings::MenuText("OK", "OK");
default: return BbSettings::MenuText("Cancel", "Отмена");
}
}
void OskEdit(TextEdit::Kind kind, std::string text = {}) {
std::scoped_lock lock{prompt_mutex};
text_edits.push_back({kind, std::move(text)});
}
void OskPress(int row, int col) {
if (row < OskKeyRows()) {
OskEdit(TextEdit::Append, OskKey(row, col));
return;
}
switch (col) {
case OskCaps: osk.caps = !osk.caps; break;
case OskSpace: OskEdit(TextEdit::Append, " "); break;
case OskBackspace: OskEdit(TextEdit::Backspace); break;
case OskClear: OskEdit(TextEdit::Clear); break;
case OskConfirm: OskEdit(TextEdit::Confirm); break;
default: OskEdit(TextEdit::Cancel); break;
}
}
void OskMove(int d_row, int d_col) {
const int rows = OskKeyRows() + 1;
osk.row = (osk.row + d_row + rows) % rows;
const int length = OskRowLength(osk.row);
osk.col = d_col ? (osk.col + d_col + length) % length : std::min(osk.col, length - 1);
}
/// Window thread, the text dialog open and the menu closed: a controller types on the keyboard.
bool OskEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
switch (event.gbutton.button) {
case SDL_GAMEPAD_BUTTON_DPAD_UP: OskMove(-1, 0); return true;
case SDL_GAMEPAD_BUTTON_DPAD_DOWN: OskMove(1, 0); return true;
case SDL_GAMEPAD_BUTTON_DPAD_LEFT: OskMove(0, -1); return true;
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: OskMove(0, 1); return true;
case SDL_GAMEPAD_BUTTON_SOUTH: OskPress(osk.row, osk.col); return true;
case SDL_GAMEPAD_BUTTON_WEST: OskEdit(TextEdit::Backspace); return true;
case SDL_GAMEPAD_BUTTON_NORTH: OskEdit(TextEdit::Append, " "); return true;
case SDL_GAMEPAD_BUTTON_EAST: OskEdit(TextEdit::Cancel); return true;
case SDL_GAMEPAD_BUTTON_START: OskEdit(TextEdit::Confirm); return true;
case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: osk.caps = !osk.caps; return true;
default: return false;
}
}
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION &&
(event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTX || event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTY)) {
constexpr int Deadzone = 18000;
const u64 now = SDL_GetTicks();
const int value = event.gaxis.value;
if (now - osk.last_stick_ms < 180 || (value < Deadzone && value > -Deadzone)) {
return true;
}
osk.last_stick_ms = now;
const int step = value > 0 ? 1 : -1;
event.gaxis.axis == SDL_GAMEPAD_AXIS_LEFTX ? OskMove(0, step) : OskMove(step, 0);
return true;
}
return false;
}
void TextPrompt() {
std::string title, text;
{
@@ -601,11 +896,12 @@ void TextPrompt() {
title = prompt_title;
text = prompt_text;
}
const float s = ImGui::GetStyle().FontScaleMain;
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(viewport->WorkPos.x + viewport->WorkSize.x * 0.5f,
viewport->WorkPos.y + viewport->WorkSize.y * 0.5f),
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowBgAlpha(0.9f);
ImGui::SetNextWindowBgAlpha(0.94f);
ImGui::Begin("##textprompt", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
@@ -614,7 +910,45 @@ void TextPrompt() {
ImGui::Separator();
ImGui::Text("%s_", text.c_str());
ImGui::Separator();
ImGui::TextUnformatted("Keyboard: type, Backspace = delete, Enter = OK, Esc = cancel");
// The keys: the selected one in the menu's accent colour. Drawn only (NoInputs): the
// controller moves the selection, a physical keyboard types as before.
const ImVec2 key(40.0f * s, 34.0f * s);
const ImGuiStyle& style = ImGui::GetStyle();
const auto key_button = [&](const char* label, float width, bool selected) {
ImGui::PushStyleColor(ImGuiCol_Button, selected ? style.Colors[ImGuiCol_ButtonActive]
: style.Colors[ImGuiCol_FrameBg]);
ImGui::Button(label, ImVec2(width, key.y));
ImGui::PopStyleColor();
};
osk.row = std::min(osk.row, OskKeyRows()); // the language changed: its row went
for (int row = 0; row < OskKeyRows(); ++row) {
for (int col = 0; col < OskRowLength(row); ++col) {
if (col) {
ImGui::SameLine();
}
ImGui::PushID(row * 16 + col);
key_button(OskKey(row, col), key.x, osk.row == row && osk.col == col);
ImGui::PopID();
}
}
for (int action = 0; action < OskActions; ++action) {
if (action) {
ImGui::SameLine();
}
const char* label = OskActionLabel(action);
const bool caps_on = action == OskCaps && osk.caps;
key_button(label, std::max(key.x * 1.4f, ImGui::CalcTextSize(label).x + 18.0f * s),
(osk.row == OskKeyRows() && osk.col == action) || caps_on);
}
ImGui::Separator();
ImGui::TextUnformatted(BbSettings::MenuText(
"Controller: D-pad or left stick moves, Cross types, Square deletes, Triangle is a space, "
"L1/R1 caps, Options = OK, Circle = cancel",
"Геймпад: крестовина или левый стик — выбор, Cross — ввод, Square — стереть, Triangle — "
"пробел, L1/R1 — регистр, Options — OK, Circle — отмена"));
ImGui::TextUnformatted(BbSettings::MenuText(
"Keyboard: type, Backspace = delete, Enter = OK, Esc = cancel",
"Клавиатура: ввод, Backspace — стереть, Enter — OK, Esc — отмена"));
ImGui::End();
}
@@ -671,6 +1005,51 @@ void ScriptedKeys() {
}
}
// bbport: the menu's own font has no Chinese characters. The first time the menu opens or speaks
// Simplified Chinese, a system font for it is merged into that font: BB_CJK_FONT (a file), else
// fontconfig's choice for zh-cn (Noto Sans CJK, WenQuanYi...). ImGui 1.92 rasterizes glyphs as
// they are drawn, so the font costs memory for its file, not a texture of every character.
// Without such a font the Chinese texts show as boxes, and the log says why.
void LoadCjkFont() {
std::string path;
int index = 0;
if (const char* file = std::getenv("BB_CJK_FONT"); file && *file) {
path = file;
} else if (FILE* pipe = popen("fc-match -f '%{file}|%{index}|%{lang}' 'sans-serif:lang=zh-cn' "
"2>/dev/null",
"r")) {
std::string out;
char buffer[4096];
for (size_t n; (n = std::fread(buffer, 1, sizeof(buffer), pipe)) > 0;) {
out.append(buffer, n);
}
pclose(pipe);
// file|index|lang|lang|...: fontconfig answers with some font even when none has
// Chinese, so its languages must list zh-cn.
const size_t first = out.find('|');
const size_t second = first == std::string::npos ? first : out.find('|', first + 1);
if (second != std::string::npos && (out + "|").find("|zh-cn|", second) != std::string::npos) {
path = out.substr(0, first);
index = std::atoi(out.c_str() + first + 1);
}
}
if (path.empty()) {
std::printf("Overlay: no font with Chinese characters found (install Noto Sans CJK or "
"WenQuanYi, or set BB_CJK_FONT to a font file)\n");
return;
}
ImGuiIO& io = ImGui::GetIO();
ImFontConfig config;
config.MergeMode = true;
config.FontNo = index;
config.DstFont = io.Fonts->Fonts[0];
if (io.Fonts->AddFontFromFileTTF(path.c_str(), 18.0f, &config)) {
std::printf("Overlay: Chinese characters from %s\n", path.c_str());
} else {
std::printf("Overlay: could not load the font %s\n", path.c_str());
}
}
} // namespace
void SetTextPrompt(bool active, const std::string& prompt, const std::string& text) {
@@ -678,10 +1057,22 @@ void SetTextPrompt(bool active, const std::string& prompt, const std::string& te
std::scoped_lock lock{prompt_mutex};
prompt_title = prompt;
prompt_text = text;
if (!active) {
text_edits.clear();
}
}
if (active && !prompt_active.exchange(true)) {
std::scoped_lock lock{imgui_mutex};
osk = OnScreenKeyboard{};
}
prompt_active = active;
}
std::vector<TextEdit> TakeTextEdits() {
std::scoped_lock lock{prompt_mutex};
return std::exchange(text_edits, {});
}
void Init(const Vulkan::Instance& instance, vk::Format format, u32 image_count) {
std::scoped_lock lock{imgui_mutex};
if (initialized) {
@@ -784,6 +1175,8 @@ bool HandleEvent(const SDL_Event& event) {
}
return true;
}
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
return !is_open && prompt_active && OskEvent(event);
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP: {
const bool down = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN;
@@ -798,7 +1191,7 @@ bool HandleEvent(const SDL_Event& event) {
return true;
}
if (!is_open) {
return false;
return prompt_active && OskEvent(event);
}
if (const ImGuiKey key = KeyFromGamepad(button); key != ImGuiKey_None) {
io.AddKeyEvent(key, down);
@@ -848,7 +1241,18 @@ bool HandleEvent(const SDL_Event& event) {
}
bool Visible() {
return initialized && (menu_open || prompt_active || BbSettings::Get().show_fps);
return initialized && (menu_open || prompt_active || notice_busy || BbSettings::Get().show_fps ||
ShaderNoticeDue());
}
void TrophyNotice(int id, char grade, std::string name, std::vector<u8> icon, int icon_size) {
if (icon.size() != size_t(icon_size) * size_t(icon_size) * 4) {
icon.clear();
icon_size = 0;
}
std::scoped_lock lock{notice_mutex};
notices.push_back(Notice{id, grade, std::move(name), std::move(icon), icon_size});
notice_busy = true;
}
bool MenuOpen() {
@@ -889,6 +1293,13 @@ void Render(vk::CommandBuffer cmdbuf, vk::ImageView view, vk::Extent2D extent) {
base_scale = scale;
}
static bool cjk_font_tried = false;
// Also when the menu opens in another language: its language list names Chinese in Chinese.
if (!cjk_font_tried &&
(menu_open || BbSettings::Get().menu_language == BbSettings::MenuLanguage::ChineseSimplified)) {
cjk_font_tried = true;
LoadCjkFont();
}
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
if (menu_open) {
@@ -897,9 +1308,15 @@ void Render(vk::CommandBuffer cmdbuf, vk::ImageView view, vk::Extent2D extent) {
if (BbSettings::Get().show_fps && !menu_open) {
FpsCounter();
}
if (!menu_open && ShaderNoticeDue()) {
ShaderNotice();
}
if (prompt_active && !menu_open) {
TextPrompt();
}
if (notice_busy) {
TrophyCard();
}
ImGui::Render();
const vk::RenderingAttachmentInfo attachment{
+13
View File
@@ -6,6 +6,7 @@
#pragma once
#include <string>
#include <vector>
#include "common/types.h"
#include "video_core/renderer_vulkan/vk_common.h"
@@ -41,4 +42,16 @@ bool MenuOpen();
/// Window thread: the game's text dialog (ImeDialog) state, drawn as a box over the frame.
void SetTextPrompt(bool active, const std::string& prompt, const std::string& text);
/// An edit typed on the text dialog's on-screen keyboard with a controller.
struct TextEdit {
enum Kind { Append, Backspace, Clear, Confirm, Cancel } kind;
std::string text; ///< Append
};
/// Window thread: the edits made on the on-screen keyboard since the last call, in order.
std::vector<TextEdit> TakeTextEdits();
/// A trophy the game unlocked: a card in the top-left corner for a few seconds (queued while one is
/// shown). `icon`: `icon_size` x `icon_size` RGBA, or empty for a cup in the grade's colour.
void TrophyNotice(int id, char grade, std::string name, std::vector<u8> icon, int icon_size);
} // namespace BbOverlay
+39 -8
View File
@@ -1,10 +1,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "bbport_settings.h"
#include "bbport_menu_i18n.h"
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iterator>
#include <string>
#include <string_view>
#include <utility>
@@ -27,9 +30,13 @@ void Set(Values& v, const std::string& key, const std::string& value) {
const float f = float(std::atof(value.c_str()));
const int i = std::atoi(value.c_str());
if (key == "menu_language") {
if (value == "en") v.menu_language = MenuLanguage::English;
else if (value == "ru") v.menu_language = MenuLanguage::Russian;
else std::printf("Settings: unknown menu_language '%s' (expected en or ru)\n", value.c_str());
const auto it = std::find(std::begin(MenuLanguageCodes), std::end(MenuLanguageCodes), value);
if (it != std::end(MenuLanguageCodes)) {
v.menu_language = MenuLanguage(it - std::begin(MenuLanguageCodes));
} else {
std::printf("Settings: unknown menu_language '%s' (expected en, ru, es, it, fr, de, pt_BR or zh_CN)\n",
value.c_str());
}
} else if (key == "upscaler") {
for (int u = 0; u < UpscalerCount; ++u) {
if (value == UpscalerName(u)) {
@@ -58,6 +65,8 @@ void Set(Values& v, const std::string& key, const std::string& value) {
v.debug_view = std::clamp(i, 0, DebugViewCount - 1);
} else if (key == "show_fps") {
v.show_fps = i != 0;
} else if (key == "shader_notice") {
v.shader_notice = i != 0;
} else if (key == "menu_pos") {
float x = -1.0f, y = -1.0f;
if (std::sscanf(value.c_str(), "%f,%f", &x, &y) == 2 && x >= 0.0f && x <= 1.0f && y >= 0.0f &&
@@ -96,7 +105,30 @@ Values& Get() {
}
const char* MenuText(const char* english, const char* russian) {
return Get().menu_language == MenuLanguage::Russian ? russian : english;
return MenuTextIn(Get().menu_language, english, russian);
}
const char* MenuTextIn(MenuLanguage language, const char* english, const char* russian) {
switch (language) {
case MenuLanguage::English:
return english;
case MenuLanguage::Russian:
return russian;
default:
return MenuTranslation(language, english);
}
}
MenuLanguage MenuLanguageFromCode(const char* code) {
if (code && code[0] && code[1]) {
for (int l = 0; l < int(MenuLanguage::Count); ++l) {
if (std::tolower(code[0]) == MenuLanguageCodes[l][0] &&
std::tolower(code[1]) == MenuLanguageCodes[l][1]) {
return MenuLanguage(l);
}
}
}
return MenuLanguage::English;
}
void Load() {
@@ -116,9 +148,7 @@ void Load() {
}
}
}
v.menu_language = lang && (lang[0] == 'r' || lang[0] == 'R') && (lang[1] == 'u' || lang[1] == 'U')
? MenuLanguage::Russian
: MenuLanguage::English;
v.menu_language = MenuLanguageFromCode(lang);
}
if (FILE* file = std::fopen(Path(), "r")) {
char line[256];
@@ -222,7 +252,7 @@ void Save() {
return std::string(text);
};
const auto flag = [](bool value) { return std::string(value ? "1" : "0"); };
put("menu_language", v.menu_language == MenuLanguage::Russian ? "ru" : "en");
put("menu_language", MenuLanguageCodes[int(v.menu_language.load())]);
put("upscaler", UpscalerName(v.upscaler));
put("preset", std::to_string(v.preset.load()));
put("sharpen", flag(v.sharpen));
@@ -235,6 +265,7 @@ void Save() {
put("reactive_max", fixed(v.reactive_max, 2));
put("debug_view", std::to_string(v.debug_view.load()));
put("show_fps", flag(v.show_fps));
put("shader_notice", flag(v.shader_notice));
put("fsr4_auto_exposure", flag(v.fsr4_auto_exposure));
put("fsr4_invert_jitter", flag(v.fsr4_invert_jitter));
// Read by patches.py at start.
+30 -2
View File
@@ -17,7 +17,17 @@ inline bool IsFsr4(int upscaler) {
}
enum Preset : int { NativeAA = 0, Quality, Balanced, Performance, UltraPerformance, PresetCount };
enum DebugView : int { DebugNone = 0, DebugReactive = 1, DebugMotion = 2, DebugViewCount };
enum class MenuLanguage { English, Russian };
/// Overlay languages, in the order of the menu's Language selector.
enum class MenuLanguage {
English, Russian, Spanish, Italian, French, German, PortugueseBrazil, ChineseSimplified, Count
};
/// bbport.ini / BB_MENU_LANGUAGE codes, indexed by MenuLanguage.
inline constexpr const char* MenuLanguageCodes[] = {"en", "ru", "es", "it", "fr", "de", "pt_BR",
"zh_CN"};
/// Each language's own name, for the selector.
inline constexpr const char* MenuLanguageNames[] = {"English", "Русский", "Español",
"Italiano", "Français", "Deutsch",
"Português (Brasil)", "简体中文"};
/// Game effects switched by the community patches at start (patches.py EFFECTS): ini key,
/// menu label, default (the game's own behaviour).
@@ -38,6 +48,17 @@ inline constexpr Effect Effects[] = {
{"skip_intro", "Skip startup intros", "Пропуск заставок при запуске", false},
{"debug_camera", "Free camera (Cross + L3)", "Свободная камера (Cross + L3)", false},
{"debug_menu", "Debug menu (requires font files)", "Debug menu (нужны файлы шрифтов)", false},
// Cheats and gameplay tweaks (launcher "Cheats" page).
{"cheat_no_death", "Cheat: never die (not below 1 HP)", "Чит: бессмертие (не ниже 1 HP)", false},
{"cheat_stealth", "Cheat: enemies do not see you", "Чит: враги не замечают", false},
{"cheat_silent", "Cheat: enemies do not hear you", "Чит: враги не слышат", false},
{"cheat_rally_no_decay", "Cheat: Rally never fades", "Чит: Rally не угасает", false},
{"cheat_enemy_control", "Cheat: enemy control (R3 / L3)", "Чит: управление врагом (R3 / L3)", false},
{"tweak_no_rally", "No Rally (health regain)", "Без Rally (возврата HP)", false},
{"tweak_camera_distance", "Camera further away", "Камера дальше", false},
{"tweak_no_camera_rotation", "No camera auto-rotation", "Без автоповорота камеры", false},
{"tweak_easy_run", "Run with less stick tilt", "Бег с меньшим наклоном стика", false},
{"tweak_ragdoll", "Dark Souls ragdoll physics", "Физика тел как в Dark Souls", false},
};
inline constexpr int EffectCount = int(sizeof(Effects) / sizeof(Effects[0]));
/// Live output resolutions: the upscaler's output and the UI host targets.
@@ -60,6 +81,8 @@ struct Values {
std::atomic<float> reactive_max{0.9f};
std::atomic<int> debug_view{DebugNone};
std::atomic<bool> show_fps{false};
/// "Compiling shaders" in the bottom-left corner while it happens (stutter is expected then).
std::atomic<bool> shader_notice{true};
/// The settings menu's position (fraction of the screen), -1 until it is moved.
std::atomic<float> menu_x{-1.0f}, menu_y{-1.0f};
// FSR 4 checks (menu): the provider's auto exposure, the jitter sign it is given.
@@ -91,8 +114,13 @@ struct Values {
};
Values& Get();
/// Localized overlay text; the Language selector itself stays in English.
/// Localized overlay text; the Language selector itself stays in English. Languages other than
/// English and Russian look the English text up in bbport_menu_i18n.h (English when missing).
const char* MenuText(const char* english, const char* russian);
/// The same text in a given language (the game's own System menu follows the game's language).
const char* MenuTextIn(MenuLanguage language, const char* english, const char* russian);
/// MenuLanguage for a code or locale ("es", "fr_FR.UTF-8"); English when unknown.
MenuLanguage MenuLanguageFromCode(const char* code);
/// Reads the file, then the environment overrides. Called once at start.
void Load();
+41
View File
@@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// bbport: whether shaders or pipelines are being compiled, for the overlay's notice. The pipeline
// cache counts every compilation (CompileTimer); the notice stays a moment after the last one
// ends, so a burst of short compilations does not make it flicker. From SrShadowy/bloodborne_pc
// (gpu/shim/bbport_shader_tracker).
#pragma once
#include <atomic>
#include <chrono>
#include <cstdint>
namespace BbShaderActivity {
inline std::atomic<std::uint32_t> active{0};
inline std::atomic<std::int64_t> last_end_ms{0};
inline std::int64_t NowMs() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
inline void Begin() {
active.fetch_add(1, std::memory_order_relaxed);
}
inline void End() {
last_end_ms.store(NowMs(), std::memory_order_relaxed);
active.fetch_sub(1, std::memory_order_release);
}
/// Milliseconds since the last compilation ended: 0 while one runs, -1 when none ever ran.
inline std::int64_t IdleMs() {
if (active.load(std::memory_order_acquire) > 0) {
return 0;
}
const std::int64_t end = last_end_ms.load(std::memory_order_relaxed);
return end == 0 ? -1 : NowMs() - end;
}
} // namespace BbShaderActivity
+9
View File
@@ -10,12 +10,21 @@
// before exec; Vulkan calls failing then are not errors: this thread waits for the exec instead.
extern "C" __attribute__((weak)) volatile int runtime_restarting; // absent in the tests
// bbport: set by the pipeline cache. Draw preparation ahead of the GPU thread reads descriptors
// the game may still be rewriting; a check failing on such a torn descriptor ends that attempt
// like a fault (the hook does not return) instead of stopping the port. Returns when no
// recovery point is armed on this thread.
void (*g_assert_speculative_exit)() = nullptr;
void assert_fail_impl() {
if (&runtime_restarting && runtime_restarting) {
for (;;) {
pause();
}
}
if (g_assert_speculative_exit) {
g_assert_speculative_exit();
}
std::fflush(stdout);
std::fputs("STOP: GPU library assertion failed (see GPU log above)\n", stderr);
std::_Exit(23);
+34
View File
@@ -162,6 +162,40 @@ bool WindowSDL::PollEvents() {
SDL_StartTextInput(window);
UpdateTextTitle();
}
// The on-screen keyboard's edits (a controller), applied here, where the text and the
// window belong.
if (text_active) {
const auto edits = BbOverlay::TakeTextEdits();
for (const auto& edit : edits) {
switch (edit.kind) {
case BbOverlay::TextEdit::Append:
text += edit.text;
break;
case BbOverlay::TextEdit::Backspace:
if (!text.empty()) {
size_t cut = text.size() - 1; // drop one UTF-8 code point
while (cut > 0 && (static_cast<unsigned char>(text[cut]) & 0xC0) == 0x80) --cut;
text.erase(cut);
}
break;
case BbOverlay::TextEdit::Clear:
text.clear();
break;
case BbOverlay::TextEdit::Confirm:
case BbOverlay::TextEdit::Cancel:
text_state = edit.kind == BbOverlay::TextEdit::Cancel ? 2 : 1;
text_active = false;
SDL_StopTextInput(window);
break;
}
if (!text_active) {
break;
}
}
if (!edits.empty()) {
UpdateTextTitle();
}
}
}
if (!text_active) {
BbOverlay::UpdateTextInput(window);
+3165 -37
View File
File diff suppressed because it is too large. Load diff
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff. Show more